|
55 | 55 | import com.google.common.collect.ImmutableList; |
56 | 56 | import com.google.common.collect.ImmutableMap; |
57 | 57 | import com.google.genai.types.Content; |
| 58 | +import com.google.genai.types.CustomMetadata; |
58 | 59 | import com.google.genai.types.Part; |
59 | 60 | import io.reactivex.rxjava3.core.Completable; |
60 | 61 | import io.reactivex.rxjava3.core.Maybe; |
61 | 62 | import java.io.IOException; |
62 | 63 | import java.time.Duration; |
63 | 64 | import java.time.Instant; |
| 65 | +import java.util.ArrayList; |
64 | 66 | import java.util.HashMap; |
| 67 | +import java.util.List; |
65 | 68 | import java.util.Map; |
66 | 69 | import java.util.Optional; |
| 70 | +import java.util.Set; |
67 | 71 | import java.util.concurrent.CompletableFuture; |
68 | 72 | import java.util.logging.Level; |
69 | 73 | import java.util.logging.Logger; |
@@ -496,6 +500,84 @@ public Maybe<Event> onEventCallback(InvocationContext invocationContext, Event e |
496 | 500 | } |
497 | 501 | } |
498 | 502 | } |
| 503 | + |
| 504 | + // --- A2A interaction logging --- |
| 505 | + if (event.customMetadata().isPresent()) { |
| 506 | + Map<String, Object> a2aKeys = new HashMap<>(); |
| 507 | + for (CustomMetadata cm : event.customMetadata().get()) { |
| 508 | + if (cm.key().isPresent() && cm.key().get().startsWith(BigQueryUtils.A2A_PREFIX)) { |
| 509 | + cm.stringValue().ifPresent(val -> a2aKeys.put(cm.key().get(), val)); |
| 510 | + } |
| 511 | + } |
| 512 | + if (a2aKeys.containsKey(BigQueryUtils.A2A_REQUEST_KEY) |
| 513 | + || a2aKeys.containsKey(BigQueryUtils.A2A_RESPONSE_KEY)) { |
| 514 | + Object responsePayload = a2aKeys.get(BigQueryUtils.A2A_RESPONSE_KEY); |
| 515 | + Object contentObject = null; |
| 516 | + boolean contentTruncated = false; |
| 517 | + if (responsePayload != null) { |
| 518 | + TruncationResult responseTruncated = |
| 519 | + smartTruncate(responsePayload, config.maxContentLength()); |
| 520 | + contentObject = toJavaObject(responseTruncated.node()); |
| 521 | + contentTruncated = responseTruncated.isTruncated(); |
| 522 | + } |
| 523 | + |
| 524 | + // Exclude a2a:response from a2a_metadata to save storage space and avoid duplication |
| 525 | + Map<String, Object> a2aMetaKeys = new HashMap<>(a2aKeys); |
| 526 | + a2aMetaKeys.remove(BigQueryUtils.A2A_RESPONSE_KEY); |
| 527 | + TruncationResult a2aTruncated = smartTruncate(a2aMetaKeys, config.maxContentLength()); |
| 528 | + |
| 529 | + Map<String, Object> extraAttributes = new HashMap<>(); |
| 530 | + Object a2aMeta = toJavaObject(a2aTruncated.node()); |
| 531 | + if (a2aMeta != null) { |
| 532 | + extraAttributes.put("a2a_metadata", a2aMeta); |
| 533 | + } |
| 534 | + |
| 535 | + logCompletable = |
| 536 | + logCompletable.andThen( |
| 537 | + logEvent( |
| 538 | + "A2A_INTERACTION", |
| 539 | + invocationContext, |
| 540 | + contentObject, |
| 541 | + a2aTruncated.isTruncated() || contentTruncated, |
| 542 | + Optional.of(EventData.builder().setExtraAttributes(extraAttributes).build()))); |
| 543 | + } |
| 544 | + } |
| 545 | + |
| 546 | + // --- Final agent response logging --- |
| 547 | + if (isFinalAgentResponse(event)) { |
| 548 | + List<Part> visibleParts = new ArrayList<>(); |
| 549 | + for (Part part : event.content().get().parts().get()) { |
| 550 | + if (part.text().isPresent() && !part.thought().orElse(false)) { |
| 551 | + visibleParts.add(part); |
| 552 | + } |
| 553 | + } |
| 554 | + if (!visibleParts.isEmpty()) { |
| 555 | + Content visibleContent = |
| 556 | + Content.builder() |
| 557 | + .role(event.content().get().role().orElse("model")) |
| 558 | + .parts(visibleParts) |
| 559 | + .build(); |
| 560 | + |
| 561 | + Map<String, Object> extraAttributes = new HashMap<>(); |
| 562 | + if (event.id() != null) { |
| 563 | + extraAttributes.put("source_event_id", event.id()); |
| 564 | + } |
| 565 | + if (event.author() != null) { |
| 566 | + extraAttributes.put("source_event_author", event.author()); |
| 567 | + } |
| 568 | + event.branch().ifPresent(branch -> extraAttributes.put("source_event_branch", branch)); |
| 569 | + |
| 570 | + logCompletable = |
| 571 | + logCompletable.andThen( |
| 572 | + logEvent( |
| 573 | + "AGENT_RESPONSE", |
| 574 | + invocationContext, |
| 575 | + visibleContent, |
| 576 | + false, |
| 577 | + Optional.of(EventData.builder().setExtraAttributes(extraAttributes).build()))); |
| 578 | + } |
| 579 | + } |
| 580 | + |
499 | 581 | return logCompletable.andThen(Maybe.empty()); |
500 | 582 | } |
501 | 583 |
|
@@ -635,6 +717,9 @@ public Maybe<LlmResponse> afterModelCallback( |
635 | 717 | usage.promptTokenCount().ifPresent(c -> usageDict.put("prompt", c)); |
636 | 718 | usage.candidatesTokenCount().ifPresent(c -> usageDict.put("completion", c)); |
637 | 719 | usage.totalTokenCount().ifPresent(c -> usageDict.put("total", c)); |
| 720 | + usage |
| 721 | + .cachedContentTokenCount() |
| 722 | + .ifPresent(c -> usageDict.put("cached_content_token_count", c)); |
638 | 723 | }); |
639 | 724 |
|
640 | 725 | InvocationContext invocationContext = callbackContext.invocationContext(); |
@@ -858,4 +943,22 @@ private String getToolOrigin(BaseTool tool) { |
858 | 943 | } |
859 | 944 | return "UNKNOWN"; |
860 | 945 | } |
| 946 | + |
| 947 | + /** |
| 948 | + * Returns true if the event represents a final agent response. |
| 949 | + * |
| 950 | + * <p>We verify finalResponse() along with empty checks for partial, function calls/responses, and |
| 951 | + * long-running tool IDs. This is required because finalResponse() would otherwise return true |
| 952 | + * even for thought-only, short-circuited skipSummarization() events (which ADK treats as |
| 953 | + * invisible internal reasoning and should not be logged as agent responses). |
| 954 | + */ |
| 955 | + private boolean isFinalAgentResponse(Event event) { |
| 956 | + return event.content().isPresent() |
| 957 | + && event.content().get().parts().isPresent() |
| 958 | + && event.finalResponse() |
| 959 | + && !event.partial().orElse(false) |
| 960 | + && event.functionCalls().isEmpty() |
| 961 | + && event.functionResponses().isEmpty() |
| 962 | + && event.longRunningToolIds().map(Set::isEmpty).orElse(true); |
| 963 | + } |
861 | 964 | } |
0 commit comments