Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions core/src/main/java/com/google/adk/events/Event.java
Original file line number Diff line number Diff line change
Expand Up @@ -419,6 +419,21 @@ && functionResponses().isEmpty()
&& !hasTrailingCodeExecutionResult();
}

/**
* Returns the name of the workflow node that emitted this event, from {@link NodeInfo#name()}.
* Empty outside a workflow and on an event that carries any agent state or ends an agent. ADK
* Python's {@code Event.node_name} returns an empty string instead and ignores an empty agent
* state.
*/
@Experimental
@JsonIgnore
public final Optional<String> nodeName() {
if (actions().agentState().isPresent() || actions().endOfAgent()) {
return Optional.empty();
}
return nodeInfo().flatMap(NodeInfo::name);
}

/**
* Converts the event content into a readable string.
*
Expand Down
49 changes: 49 additions & 0 deletions core/src/main/java/com/google/adk/workflow/NodeInfo.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
package com.google.adk.workflow;

import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonSetter;
Expand All @@ -27,6 +28,7 @@
import com.google.common.collect.ImmutableList;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import java.util.List;
import java.util.Optional;

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

/**
* Returns the emitting node's name: the last segment of {@link #path()} without its run ID, such
* as {@code a} for {@code wf@1/a@2}, or empty when the path is empty. ADK Python's {@code
* NodeInfo.name} returns an empty string instead.
*/
@JsonIgnore
public final Optional<String> name() {
if (path().isEmpty()) {
return Optional.empty();
}
String segment = lastSegment(path());
int at = segment.lastIndexOf('@');
return Optional.of(at < 0 ? segment : segment.substring(0, at));
}

/**
* Returns the emitting node's run ID: the part after {@code @} in the last segment of {@link
* #path()}, such as {@code 2} for {@code wf@1/a@2}, or empty when the segment has none. ADK
* Python's {@code NodeInfo.run_id} returns an empty string instead.
*/
@JsonIgnore
public final Optional<String> runId() {
return runIdOf(lastSegment(path()));
}

/**
* Returns the run ID of the emitting node's parent: the part after {@code @} in the
* second-to-last segment of {@link #path()}, such as {@code 1} for {@code wf@1/a@2}. Empty when
* the path has no parent segment or that segment has no run ID, as in ADK Python's {@code
* NodeInfo.parent_run_id}.
*/
@JsonIgnore
public final Optional<String> parentRunId() {
String path = path();
int lastSlash = path.lastIndexOf('/');
return lastSlash < 0 ? Optional.empty() : runIdOf(lastSegment(path.substring(0, lastSlash)));
}

private static String lastSegment(String path) {
return path.substring(path.lastIndexOf('/') + 1);
}

private static Optional<String> runIdOf(String segment) {
int at = segment.lastIndexOf('@');
return at < 0 ? Optional.empty() : Optional.of(segment.substring(at + 1));
}

public static Builder builder() {
return new AutoValue_NodeInfo.Builder()
.path("")
Expand Down
30 changes: 30 additions & 0 deletions core/src/test/java/com/google/adk/events/EventTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,36 @@ public void json_roundTripsAnEmptyNodeInfo() {
assertThat(Event.fromJson(event.toJson())).isEqualTo(event);
}

@Test
public void nodeName_isTheEmittingNodesName() {
Event event = EVENT.toBuilder().nodeInfo(NodeInfo.builder().path("wf@1/a@2").build()).build();

assertThat(event.nodeName()).hasValue("a");
}

@Test
public void nodeName_emptyOutsideAWorkflow() {
assertThat(EVENT.nodeName()).isEmpty();
assertThat(EVENT.toBuilder().nodeInfo(NodeInfo.builder().build()).build().nodeName()).isEmpty();
}

@Test
public void nodeName_emptyForAgentStateAndEndOfAgentEvents() {
Event endOfAgent =
EVENT.toBuilder()
.nodeInfo(NODE_INFO)
.actions(EventActions.builder().endOfAgent(true).build())
.build();
Event agentState =
EVENT.toBuilder()
.nodeInfo(NODE_INFO)
.actions(EventActions.builder().agentState(ImmutableMap.of()).build())
.build();

assertThat(endOfAgent.nodeName()).isEmpty();
assertThat(agentState.nodeName()).isEmpty();
}

@Test
public void event_builder_with_transcriptions_works() {
Transcription inputTranscription =
Expand Down
59 changes: 59 additions & 0 deletions core/src/test/java/com/google/adk/workflow/NodeInfoTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@

import com.google.adk.JsonBaseModel;
import com.google.common.collect.ImmutableList;
import java.util.Map;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
Expand Down Expand Up @@ -63,4 +64,62 @@ public void json_omitsUnsetOutputForAndFalseMessageAsOutput() {
assertThat(json).doesNotContain("outputFor");
assertThat(json).doesNotContain("messageAsOutput");
}

@Test
public void json_omitsPathHelpers() {
Map<?, ?> json = JsonBaseModel.getMapper().convertValue(NODE_INFO, Map.class);

assertThat(json.keySet()).containsExactly("path", "outputFor", "messageAsOutput");
}

@Test
public void name_isTheLastSegmentWithoutItsRunId() {
assertThat(nodeInfo("wf@1/a@2/b@3").name()).hasValue("b");
assertThat(nodeInfo("wf@1/b").name()).hasValue("b");
assertThat(nodeInfo("wf@1").name()).hasValue("wf");
}

@Test
public void runId_isTheLastSegmentsRunId() {
assertThat(nodeInfo("wf@1/a@2/b@3").runId()).hasValue("3");
}

@Test
public void runId_emptyWithoutARunId() {
assertThat(nodeInfo("wf@1/b").runId()).isEmpty();
}

@Test
public void parentRunId_isTheSecondToLastSegmentsRunId() {
assertThat(nodeInfo("wf@1/a@2/b@3").parentRunId()).hasValue("2");
assertThat(nodeInfo("wf@1/b").parentRunId()).hasValue("1");
}

@Test
public void parentRunId_emptyWithoutAParentRunId() {
assertThat(nodeInfo("wf@1").parentRunId()).isEmpty();
assertThat(nodeInfo("wf/b@1").parentRunId()).isEmpty();
}

@Test
public void pathHelpers_splitEachSegmentAtItsLastAt() {
NodeInfo nodeInfo = nodeInfo("w@f@1/a@b@2");

assertThat(nodeInfo.name()).hasValue("a@b");
assertThat(nodeInfo.runId()).hasValue("2");
assertThat(nodeInfo.parentRunId()).hasValue("1");
}

@Test
public void pathHelpers_emptyForAnEmptyPath() {
NodeInfo nodeInfo = NodeInfo.builder().build();

assertThat(nodeInfo.name()).isEmpty();
assertThat(nodeInfo.runId()).isEmpty();
assertThat(nodeInfo.parentRunId()).isEmpty();
}

private static NodeInfo nodeInfo(String path) {
return NodeInfo.builder().path(path).build();
}
}
Loading