Skip to content

Commit 52f2be7

Browse files
damianmomotgooglecopybara-github
authored andcommitted
feat: add NodeInfo.name, runId, and parentRunId, and Event.nodeName
PiperOrigin-RevId: 1004687606
1 parent e293898 commit 52f2be7

4 files changed

Lines changed: 153 additions & 0 deletions

File tree

‎core/src/main/java/com/google/adk/events/Event.java‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -419,6 +419,21 @@ && functionResponses().isEmpty()
419419
&& !hasTrailingCodeExecutionResult();
420420
}
421421

422+
/**
423+
* Returns the name of the workflow node that emitted this event, from {@link NodeInfo#name()}.
424+
* Empty outside a workflow and on an event that carries any agent state or ends an agent. ADK
425+
* Python's {@code Event.node_name} returns an empty string instead and ignores an empty agent
426+
* state.
427+
*/
428+
@Experimental
429+
@JsonIgnore
430+
public final Optional<String> nodeName() {
431+
if (actions().agentState().isPresent() || actions().endOfAgent()) {
432+
return Optional.empty();
433+
}
434+
return nodeInfo().flatMap(NodeInfo::name);
435+
}
436+
422437
/**
423438
* Converts the event content into a readable string.
424439
*

‎core/src/main/java/com/google/adk/workflow/NodeInfo.java‎

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
package com.google.adk.workflow;
1818

1919
import com.fasterxml.jackson.annotation.JsonCreator;
20+
import com.fasterxml.jackson.annotation.JsonIgnore;
2021
import com.fasterxml.jackson.annotation.JsonInclude;
2122
import com.fasterxml.jackson.annotation.JsonProperty;
2223
import com.fasterxml.jackson.annotation.JsonSetter;
@@ -27,6 +28,7 @@
2728
import com.google.common.collect.ImmutableList;
2829
import com.google.errorprone.annotations.CanIgnoreReturnValue;
2930
import java.util.List;
31+
import java.util.Optional;
3032

3133
/**
3234
* The identity of the workflow-node activation that emitted an {@link com.google.adk.events.Event}.
@@ -64,6 +66,53 @@ public abstract class NodeInfo {
6466
@JsonInclude(JsonInclude.Include.NON_DEFAULT)
6567
public abstract boolean messageAsOutput();
6668

69+
/**
70+
* Returns the emitting node's name: the last segment of {@link #path()} without its run ID, such
71+
* as {@code a} for {@code wf@1/a@2}, or empty when the path is empty. ADK Python's {@code
72+
* NodeInfo.name} returns an empty string instead.
73+
*/
74+
@JsonIgnore
75+
public final Optional<String> name() {
76+
if (path().isEmpty()) {
77+
return Optional.empty();
78+
}
79+
String segment = lastSegment(path());
80+
int at = segment.lastIndexOf('@');
81+
return Optional.of(at < 0 ? segment : segment.substring(0, at));
82+
}
83+
84+
/**
85+
* Returns the emitting node's run ID: the part after {@code @} in the last segment of {@link
86+
* #path()}, such as {@code 2} for {@code wf@1/a@2}, or empty when the segment has none. ADK
87+
* Python's {@code NodeInfo.run_id} returns an empty string instead.
88+
*/
89+
@JsonIgnore
90+
public final Optional<String> runId() {
91+
return runIdOf(lastSegment(path()));
92+
}
93+
94+
/**
95+
* Returns the run ID of the emitting node's parent: the part after {@code @} in the
96+
* second-to-last segment of {@link #path()}, such as {@code 1} for {@code wf@1/a@2}. Empty when
97+
* the path has no parent segment or that segment has no run ID, as in ADK Python's {@code
98+
* NodeInfo.parent_run_id}.
99+
*/
100+
@JsonIgnore
101+
public final Optional<String> parentRunId() {
102+
String path = path();
103+
int lastSlash = path.lastIndexOf('/');
104+
return lastSlash < 0 ? Optional.empty() : runIdOf(lastSegment(path.substring(0, lastSlash)));
105+
}
106+
107+
private static String lastSegment(String path) {
108+
return path.substring(path.lastIndexOf('/') + 1);
109+
}
110+
111+
private static Optional<String> runIdOf(String segment) {
112+
int at = segment.lastIndexOf('@');
113+
return at < 0 ? Optional.empty() : Optional.of(segment.substring(at + 1));
114+
}
115+
67116
public static Builder builder() {
68117
return new AutoValue_NodeInfo.Builder()
69118
.path("")

‎core/src/test/java/com/google/adk/events/EventTest.java‎

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -275,6 +275,36 @@ public void json_roundTripsAnEmptyNodeInfo() {
275275
assertThat(Event.fromJson(event.toJson())).isEqualTo(event);
276276
}
277277

278+
@Test
279+
public void nodeName_isTheEmittingNodesName() {
280+
Event event = EVENT.toBuilder().nodeInfo(NodeInfo.builder().path("wf@1/a@2").build()).build();
281+
282+
assertThat(event.nodeName()).hasValue("a");
283+
}
284+
285+
@Test
286+
public void nodeName_emptyOutsideAWorkflow() {
287+
assertThat(EVENT.nodeName()).isEmpty();
288+
assertThat(EVENT.toBuilder().nodeInfo(NodeInfo.builder().build()).build().nodeName()).isEmpty();
289+
}
290+
291+
@Test
292+
public void nodeName_emptyForAgentStateAndEndOfAgentEvents() {
293+
Event endOfAgent =
294+
EVENT.toBuilder()
295+
.nodeInfo(NODE_INFO)
296+
.actions(EventActions.builder().endOfAgent(true).build())
297+
.build();
298+
Event agentState =
299+
EVENT.toBuilder()
300+
.nodeInfo(NODE_INFO)
301+
.actions(EventActions.builder().agentState(ImmutableMap.of()).build())
302+
.build();
303+
304+
assertThat(endOfAgent.nodeName()).isEmpty();
305+
assertThat(agentState.nodeName()).isEmpty();
306+
}
307+
278308
@Test
279309
public void event_builder_with_transcriptions_works() {
280310
Transcription inputTranscription =

‎core/src/test/java/com/google/adk/workflow/NodeInfoTest.java‎

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020

2121
import com.google.adk.JsonBaseModel;
2222
import com.google.common.collect.ImmutableList;
23+
import java.util.Map;
2324
import org.junit.Test;
2425
import org.junit.runner.RunWith;
2526
import org.junit.runners.JUnit4;
@@ -63,4 +64,62 @@ public void json_omitsUnsetOutputForAndFalseMessageAsOutput() {
6364
assertThat(json).doesNotContain("outputFor");
6465
assertThat(json).doesNotContain("messageAsOutput");
6566
}
67+
68+
@Test
69+
public void json_omitsPathHelpers() {
70+
Map<?, ?> json = JsonBaseModel.getMapper().convertValue(NODE_INFO, Map.class);
71+
72+
assertThat(json.keySet()).containsExactly("path", "outputFor", "messageAsOutput");
73+
}
74+
75+
@Test
76+
public void name_isTheLastSegmentWithoutItsRunId() {
77+
assertThat(nodeInfo("wf@1/a@2/b@3").name()).hasValue("b");
78+
assertThat(nodeInfo("wf@1/b").name()).hasValue("b");
79+
assertThat(nodeInfo("wf@1").name()).hasValue("wf");
80+
}
81+
82+
@Test
83+
public void runId_isTheLastSegmentsRunId() {
84+
assertThat(nodeInfo("wf@1/a@2/b@3").runId()).hasValue("3");
85+
}
86+
87+
@Test
88+
public void runId_emptyWithoutARunId() {
89+
assertThat(nodeInfo("wf@1/b").runId()).isEmpty();
90+
}
91+
92+
@Test
93+
public void parentRunId_isTheSecondToLastSegmentsRunId() {
94+
assertThat(nodeInfo("wf@1/a@2/b@3").parentRunId()).hasValue("2");
95+
assertThat(nodeInfo("wf@1/b").parentRunId()).hasValue("1");
96+
}
97+
98+
@Test
99+
public void parentRunId_emptyWithoutAParentRunId() {
100+
assertThat(nodeInfo("wf@1").parentRunId()).isEmpty();
101+
assertThat(nodeInfo("wf/b@1").parentRunId()).isEmpty();
102+
}
103+
104+
@Test
105+
public void pathHelpers_splitEachSegmentAtItsLastAt() {
106+
NodeInfo nodeInfo = nodeInfo("w@f@1/a@b@2");
107+
108+
assertThat(nodeInfo.name()).hasValue("a@b");
109+
assertThat(nodeInfo.runId()).hasValue("2");
110+
assertThat(nodeInfo.parentRunId()).hasValue("1");
111+
}
112+
113+
@Test
114+
public void pathHelpers_emptyForAnEmptyPath() {
115+
NodeInfo nodeInfo = NodeInfo.builder().build();
116+
117+
assertThat(nodeInfo.name()).isEmpty();
118+
assertThat(nodeInfo.runId()).isEmpty();
119+
assertThat(nodeInfo.parentRunId()).isEmpty();
120+
}
121+
122+
private static NodeInfo nodeInfo(String path) {
123+
return NodeInfo.builder().path(path).build();
124+
}
66125
}

0 commit comments

Comments
 (0)