Skip to content

Commit dfdfe3c

Browse files
google-genai-botcopybara-github
authored andcommitted
feat: Enhance BigQuery Agent Analytics Plugin with new event types
This change adds logging for A2A interactions and final agent responses to BigQuery. It also enhances usage_data. PiperOrigin-RevId: 926280309
1 parent 0a40557 commit dfdfe3c

3 files changed

Lines changed: 395 additions & 0 deletions

File tree

core/src/main/java/com/google/adk/plugins/agentanalytics/BigQueryAgentAnalyticsPlugin.java

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,15 +55,19 @@
5555
import com.google.common.collect.ImmutableList;
5656
import com.google.common.collect.ImmutableMap;
5757
import com.google.genai.types.Content;
58+
import com.google.genai.types.CustomMetadata;
5859
import com.google.genai.types.Part;
5960
import io.reactivex.rxjava3.core.Completable;
6061
import io.reactivex.rxjava3.core.Maybe;
6162
import java.io.IOException;
6263
import java.time.Duration;
6364
import java.time.Instant;
65+
import java.util.ArrayList;
6466
import java.util.HashMap;
67+
import java.util.List;
6568
import java.util.Map;
6669
import java.util.Optional;
70+
import java.util.Set;
6771
import java.util.concurrent.CompletableFuture;
6872
import java.util.logging.Level;
6973
import java.util.logging.Logger;
@@ -496,6 +500,84 @@ public Maybe<Event> onEventCallback(InvocationContext invocationContext, Event e
496500
}
497501
}
498502
}
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+
499581
return logCompletable.andThen(Maybe.empty());
500582
}
501583

@@ -635,6 +717,9 @@ public Maybe<LlmResponse> afterModelCallback(
635717
usage.promptTokenCount().ifPresent(c -> usageDict.put("prompt", c));
636718
usage.candidatesTokenCount().ifPresent(c -> usageDict.put("completion", c));
637719
usage.totalTokenCount().ifPresent(c -> usageDict.put("total", c));
720+
usage
721+
.cachedContentTokenCount()
722+
.ifPresent(c -> usageDict.put("cached_content_token_count", c));
638723
});
639724

640725
InvocationContext invocationContext = callbackContext.invocationContext();
@@ -858,4 +943,22 @@ private String getToolOrigin(BaseTool tool) {
858943
}
859944
return "UNKNOWN";
860945
}
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+
}
861964
}

core/src/main/java/com/google/adk/plugins/agentanalytics/BigQueryUtils.java

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,12 @@
4747
final class BigQueryUtils {
4848
private static final Logger logger = Logger.getLogger(BigQueryUtils.class.getName());
4949

50+
static final String A2A_PREFIX = "a2a:";
51+
static final String A2A_REQUEST_KEY = "a2a:request";
52+
static final String A2A_RESPONSE_KEY = "a2a:response";
53+
static final String A2A_TASK_ID_KEY = "a2a:task_id";
54+
static final String A2A_CONTEXT_ID_KEY = "a2a:context_id";
55+
5056
private static final ImmutableList<String> VIEW_COMMON_COLUMNS =
5157
ImmutableList.of(
5258
"timestamp",
@@ -82,6 +88,12 @@ final class BigQueryUtils {
8288
"CAST(JSON_VALUE(content, '$.usage.completion') AS INT64) AS"
8389
+ " usage_completion_tokens",
8490
"CAST(JSON_VALUE(content, '$.usage.total') AS INT64) AS usage_total_tokens",
91+
"CAST(JSON_VALUE(attributes, '$.usage_metadata.cached_content_token_count') AS"
92+
+ " INT64) AS usage_cached_tokens",
93+
"SAFE_DIVIDE(CAST(JSON_VALUE(attributes,"
94+
+ " '$.usage_metadata.cached_content_token_count') AS INT64),"
95+
+ "CAST(JSON_VALUE(content, '$.usage.prompt') AS INT64)) AS"
96+
+ " context_cache_hit_rate",
8597
"CAST(JSON_VALUE(latency_ms, '$.total_ms') AS INT64) AS total_ms",
8698
"CAST(JSON_VALUE(latency_ms, '$.time_to_first_token_ms') AS INT64) AS ttft_ms",
8799
"JSON_VALUE(attributes, '$.model_version') AS model_version",
@@ -135,6 +147,29 @@ final class BigQueryUtils {
135147
ImmutableList.of(
136148
"JSON_VALUE(content, '$.tool') AS tool_name",
137149
"JSON_QUERY(content, '$.args') AS tool_args"))
150+
.put(
151+
"A2A_INTERACTION",
152+
ImmutableList.of(
153+
"content AS response_content",
154+
"JSON_VALUE(attributes, '$.a2a_metadata.\""
155+
+ A2A_TASK_ID_KEY
156+
+ "\"') AS"
157+
+ " a2a_task_id",
158+
"JSON_VALUE(attributes, '$.a2a_metadata.\""
159+
+ A2A_CONTEXT_ID_KEY
160+
+ "\"') AS"
161+
+ " a2a_context_id",
162+
"JSON_QUERY(attributes, '$.a2a_metadata.\""
163+
+ A2A_REQUEST_KEY
164+
+ "\"') AS"
165+
+ " a2a_request"))
166+
.put(
167+
"AGENT_RESPONSE",
168+
ImmutableList.of(
169+
"JSON_VALUE(content, '$.text_summary') AS text_summary",
170+
"JSON_VALUE(attributes, '$.source_event_id') AS source_event_id",
171+
"JSON_VALUE(attributes, '$.source_event_author') AS source_event_author",
172+
"JSON_VALUE(attributes, '$.source_event_branch') AS source_event_branch"))
138173
.buildOrThrow();
139174

140175
private static final String FRAMEWORK_PREFIX = "google-adk-bq-logger-java";

0 commit comments

Comments
 (0)