Skip to content

Commit c184615

Browse files
jaycee-licopybara-github
authored andcommitted
feat: add Content.text() quick accessor
PiperOrigin-RevId: 757886339
1 parent eafedd7 commit c184615

File tree

3 files changed

+125
-54
lines changed

3 files changed

+125
-54
lines changed

src/main/java/com/google/genai/types/Content.java

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,12 @@
2323
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
2424
import com.google.auto.value.AutoValue;
2525
import com.google.genai.JsonSerializable;
26+
import java.util.ArrayList;
2627
import java.util.Arrays;
2728
import java.util.List;
2829
import java.util.Optional;
30+
import java.util.logging.Logger;
31+
import org.jspecify.annotations.Nullable;
2932

3033
/** Contains the multi-part content of a message. */
3134
@AutoValue
@@ -76,8 +79,72 @@ public static Content fromJson(String jsonString) {
7679
return JsonSerializable.fromJsonString(jsonString, Content.class);
7780
}
7881

82+
private static final Logger logger = Logger.getLogger(Content.class.getName());
83+
7984
/** Constructs a Content from parts, assuming the role is "user". */
8085
public static Content fromParts(Part... parts) {
8186
return builder().role("user").parts(Arrays.asList(parts)).build();
8287
}
88+
89+
/**
90+
* Returns the concatenation of all text parts in this content.
91+
*
92+
* <p>Returns null if there are no parts in the content. Returns an empty string if parts exists
93+
* but none of the parts contain text.
94+
*/
95+
public @Nullable String text() {
96+
return aggregateTextFromParts(parts().orElse(null));
97+
}
98+
99+
/**
100+
* Aggregates all text parts in a list of parts.
101+
*
102+
* <p>Returns null if there are no parts in the list. Returns an empty string if parts exists but
103+
* none of the parts contain text.
104+
*/
105+
static @Nullable String aggregateTextFromParts(List<Part> parts) {
106+
if (parts == null || parts.isEmpty()) {
107+
return null;
108+
}
109+
110+
StringBuilder sb = new StringBuilder();
111+
ArrayList<String> nonTextParts = new ArrayList<>();
112+
for (Part part : parts) {
113+
if (part.inlineData().isPresent()) {
114+
nonTextParts.add("inlineData");
115+
}
116+
if (part.codeExecutionResult().isPresent()) {
117+
nonTextParts.add("codeExecutionResult");
118+
}
119+
if (part.executableCode().isPresent()) {
120+
nonTextParts.add("executableCode");
121+
}
122+
if (part.fileData().isPresent()) {
123+
nonTextParts.add("fileData");
124+
}
125+
if (part.functionCall().isPresent()) {
126+
nonTextParts.add("functionCall");
127+
}
128+
if (part.functionResponse().isPresent()) {
129+
nonTextParts.add("functionResponse");
130+
}
131+
if (part.videoMetadata().isPresent()) {
132+
nonTextParts.add("videoMetadata");
133+
}
134+
if (part.thought().orElse(false)) {
135+
continue;
136+
}
137+
sb.append(part.text().orElse(""));
138+
}
139+
140+
if (!nonTextParts.isEmpty()) {
141+
logger.warning(
142+
String.format(
143+
"There are non-text parts %s in the content, returning concatenation of all text"
144+
+ " parts. Please refer to the non text parts for a full response from model.",
145+
String.join(", ", nonTextParts)));
146+
}
147+
148+
return sb.toString();
149+
}
83150
}

src/main/java/com/google/genai/types/GenerateContentResponse.java

Lines changed: 1 addition & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -144,50 +144,7 @@ public static GenerateContentResponse fromJson(String jsonString) {
144144
* content.
145145
*/
146146
public @Nullable String text() {
147-
ImmutableList<Part> parts = parts();
148-
if (parts == null || parts.isEmpty()) {
149-
return null;
150-
}
151-
152-
String text = "";
153-
ArrayList<String> nonTextParts = new ArrayList<>();
154-
for (Part part : parts) {
155-
if (part.inlineData().isPresent()) {
156-
nonTextParts.add("inlineData");
157-
}
158-
if (part.codeExecutionResult().isPresent()) {
159-
nonTextParts.add("codeExecutionResult");
160-
}
161-
if (part.executableCode().isPresent()) {
162-
nonTextParts.add("executableCode");
163-
}
164-
if (part.fileData().isPresent()) {
165-
nonTextParts.add("fileData");
166-
}
167-
if (part.functionCall().isPresent()) {
168-
nonTextParts.add("functionCall");
169-
}
170-
if (part.functionResponse().isPresent()) {
171-
nonTextParts.add("functionResponse");
172-
}
173-
if (part.videoMetadata().isPresent()) {
174-
nonTextParts.add("videoMetadata");
175-
}
176-
if (part.thought().orElse(false)) {
177-
continue;
178-
}
179-
text += part.text().orElse("");
180-
}
181-
182-
if (!nonTextParts.isEmpty()) {
183-
logger.warning(
184-
String.format(
185-
"There are non-text parts %s in the response, returning concatenation of all text"
186-
+ " parts. Please refer to the non text parts for a full response from model.",
187-
String.join(", ", nonTextParts)));
188-
}
189-
190-
return text;
147+
return Content.aggregateTextFromParts(parts());
191148
}
192149

193150
/**

src/test/java/com/google/genai/types/ContentTest.java

Lines changed: 57 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -24,24 +24,71 @@
2424

2525
public class ContentTest {
2626

27+
private static final String TEXT_1 = "test-text-1";
28+
private static final String TEXT_2 = "test-text-2";
29+
private static final Part TEXT_PART_1 = Part.fromText(TEXT_1);
30+
private static final Part TEXT_PART_2 = Part.fromText(TEXT_2);
31+
private static final Part FILE_PART = Part.fromUri("test-uri", "test-mime-type");
32+
private static final Part INLINE_DATA_PART =
33+
Part.fromBytes(new byte[] {1, 2, 3}, "test-mime-type");
34+
private static final Part FUNCTION_CALL_PART =
35+
Part.fromFunctionCall("test-function-name", ImmutableMap.of("test-key", "test-value"));
36+
private static final Part FUNCTION_RESPONSE_PART =
37+
Part.fromFunctionResponse("test-function-name", ImmutableMap.of("test-key", "test-value"));
38+
2739
@Test
2840
public void testContentFromParts() {
29-
Part textPart = Part.fromText("test-text");
30-
Part filePart = Part.fromUri("test-uri", "test-mime-type");
31-
Part inlineDataPart = Part.fromBytes(new byte[] {1, 2, 3}, "test-mime-type");
32-
Part functionCallPart =
33-
Part.fromFunctionCall("test-function-name", ImmutableMap.of("test-key", "test-value"));
34-
Part functionResponsePart =
35-
Part.fromFunctionResponse("test-function-name", ImmutableMap.of("test-key", "test-value"));
36-
3741
Content content =
3842
Content.fromParts(
39-
textPart, filePart, inlineDataPart, functionCallPart, functionResponsePart);
43+
TEXT_PART_1, FILE_PART, INLINE_DATA_PART, FUNCTION_CALL_PART, FUNCTION_RESPONSE_PART);
4044

4145
assertEquals(
4246
ImmutableList.of(
43-
textPart, filePart, inlineDataPart, functionCallPart, functionResponsePart),
47+
TEXT_PART_1, FILE_PART, INLINE_DATA_PART, FUNCTION_CALL_PART, FUNCTION_RESPONSE_PART),
4448
content.parts().get());
4549
assertEquals("user", content.role().get());
4650
}
51+
52+
@Test
53+
public void testText_ConcatenatesTextParts() {
54+
Content content = Content.fromParts(TEXT_PART_1, TEXT_PART_2);
55+
String text = content.text();
56+
57+
assertEquals(TEXT_1 + TEXT_2, text);
58+
}
59+
60+
@Test
61+
public void testText_NullParts() {
62+
Content content = Content.builder().build();
63+
String text = content.text();
64+
65+
assertEquals(null, text);
66+
}
67+
68+
@Test
69+
public void testText_EmptyParts() {
70+
Content content = Content.builder().parts(ImmutableList.of()).build();
71+
String text = content.text();
72+
73+
assertEquals(null, text);
74+
}
75+
76+
@Test
77+
public void testText_NonTextParts() {
78+
Content content =
79+
Content.fromParts(
80+
TEXT_PART_1, FILE_PART, INLINE_DATA_PART, FUNCTION_CALL_PART, FUNCTION_RESPONSE_PART);
81+
String text = content.text();
82+
83+
assertEquals(TEXT_1, text);
84+
}
85+
86+
@Test
87+
public void testText_NoTextParts() {
88+
Content content =
89+
Content.fromParts(FILE_PART, INLINE_DATA_PART, FUNCTION_CALL_PART, FUNCTION_RESPONSE_PART);
90+
String text = content.text();
91+
92+
assertEquals("", text);
93+
}
4794
}

0 commit comments

Comments
 (0)