Skip to content
Draft
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
1 change: 1 addition & 0 deletions instrumentation-api-incubator/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ dependencies {
api("io.opentelemetry:opentelemetry-api-incubator")

compileOnly("com.google.auto.value:auto-value-annotations")
compileOnly("com.fasterxml.jackson.core:jackson-databind")
annotationProcessor("com.google.auto.value:auto-value")

testImplementation(project(":testing-common"))
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.instrumentation.api.incubator.semconv.genai.messages;

import com.fasterxml.jackson.annotation.JsonClassDescription;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonPropertyDescription;
import com.google.auto.value.AutoValue;
import java.util.Map;

/**
* Represents an arbitrary message part with any type and properties. This allows for extensibility
* with custom message part types.
*/
@AutoValue
@JsonClassDescription("Generic part")
public abstract class GenericPart implements MessagePart {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

jackson annotations or io.opentelemetry.api.common.Value?


@JsonProperty(required = true, value = "type")
@JsonPropertyDescription("The type of the content captured in this part")
public abstract String getType();

public static GenericPart create(String type) {
return new AutoValue_GenericPart(type);
}

public static GenericPart create(String type, Map<String, Object> additionalProperties) {
return new AutoValue_GenericPart(type);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.instrumentation.api.incubator.semconv.genai.messages;

import com.fasterxml.jackson.annotation.JsonClassDescription;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonPropertyDescription;
import com.google.auto.value.AutoValue;
import java.util.List;

/** Represents an input message sent to the model. */
@AutoValue
@JsonClassDescription("Input message")
public abstract class InputMessage {

@JsonProperty(required = true, value = "role")
@JsonPropertyDescription("Role of the entity that created the message")
public abstract String getRole();

@JsonProperty(required = true, value = "parts")
@JsonPropertyDescription("List of message parts that make up the message content")
public abstract List<MessagePart> getParts();

public static InputMessage create(String role, List<MessagePart> parts) {
return new AutoValue_InputMessage(role, parts);
}

public static InputMessage create(Role role, List<MessagePart> parts) {
return new AutoValue_InputMessage(role.getValue(), parts);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.instrumentation.api.incubator.semconv.genai.messages;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.auto.value.AutoValue;
import java.util.ArrayList;
import java.util.List;

/** Represents a collection of input messages sent to the model. */
@AutoValue
public abstract class InputMessages {

public abstract List<InputMessage> getMessages();

public static InputMessages create() {
return new AutoValue_InputMessages(new ArrayList<>());
}

public static InputMessages create(List<InputMessage> messages) {
return new AutoValue_InputMessages(new ArrayList<>(messages));
}

public InputMessages append(InputMessage inputMessage) {
List<InputMessage> messages = getMessages();
messages.add(inputMessage);
return this;
}

public String toJsonString() {
try {
ObjectMapper mapper = new ObjectMapper();
return mapper.writeValueAsString(this);
} catch (JsonProcessingException e) {
throw new RuntimeException("Failed to serialize InputMessages to JSON", e);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.instrumentation.api.incubator.semconv.genai.messages;

/** Interface for all message parts. */
public interface MessagePart {

/**
* Get the type of this message part.
*
* @return the type string
*/
String getType();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.instrumentation.api.incubator.semconv.genai.messages;

import com.fasterxml.jackson.annotation.JsonClassDescription;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonPropertyDescription;
import com.google.auto.value.AutoValue;
import java.util.List;

@AutoValue
@JsonClassDescription("Output message")
public abstract class OutputMessage {

@JsonProperty(required = true, value = "role")
@JsonPropertyDescription("Role of response")
public abstract String getRole();

@JsonProperty(required = true, value = "parts")
@JsonPropertyDescription("List of message parts that make up the message content")
public abstract List<MessagePart> getParts();

@JsonProperty(required = true, value = "finish_reason")
@JsonPropertyDescription("Reason for finishing the generation")
public abstract String getFinishReason();

public static OutputMessage create(String role, List<MessagePart> parts, String finishReason) {
return new AutoValue_OutputMessage(role, parts, finishReason);
}

public static OutputMessage create(Role role, List<MessagePart> parts, String finishReason) {
return new AutoValue_OutputMessage(role.getValue(), parts, finishReason);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.instrumentation.api.incubator.semconv.genai.messages;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.auto.value.AutoValue;
import java.util.ArrayList;
import java.util.List;

/** Represents a collection of output messages from the model. */
@AutoValue
public abstract class OutputMessages {

public abstract List<OutputMessage> getMessages();

public static OutputMessages create() {
return new AutoValue_OutputMessages(new ArrayList<>());
}

public static OutputMessages create(List<OutputMessage> messages) {
return new AutoValue_OutputMessages(new ArrayList<>(messages));
}

public OutputMessages append(OutputMessage outputMessage) {
List<OutputMessage> currentMessages = new ArrayList<>(getMessages());
currentMessages.add(outputMessage);
return new AutoValue_OutputMessages(currentMessages);
}

public String toJsonString() {
try {
ObjectMapper mapper = new ObjectMapper();
return mapper.writeValueAsString(this);
} catch (JsonProcessingException e) {
throw new RuntimeException("Failed to serialize OutputMessages to JSON", e);
}
}

/**
* Merges a chunk OutputMessage into the existing messages at the specified index. This method is
* used for streaming responses where content is received in chunks.
*
* @param index the index of the message to merge into
* @param chunkMessage the chunk message to append
* @return a new OutputMessages instance with the merged content
*/
public OutputMessages merge(int index, OutputMessage chunkMessage) {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If possible, abstracting all StreamListener types into a single interface might be a better choice. It should have an onChunk(OutputMessages) method, an onEnd() method, and an onError(Throwable) method. We can complete the common collection logic here uniformly — such as the aggregation of OutputMessages, the captures of usage tokens and time per output chunk , etc.

How do you think about that? cc @anuraaga

List<OutputMessage> currentMessages = new ArrayList<>(getMessages());

if (index < 0 || index >= currentMessages.size()) {
throw new IllegalArgumentException(
"Index "
+ index
+ " is out of bounds for messages list of size "
+ currentMessages.size());
}

OutputMessage existingMessage = currentMessages.get(index);

// Merge the parts by appending text content from chunk to existing message
List<MessagePart> mergedParts = new ArrayList<>(existingMessage.getParts());

// If the chunk message has text parts, append their content to the first text part of existing
// message
for (MessagePart chunkPart : chunkMessage.getParts()) {
if (chunkPart instanceof TextPart) {
TextPart chunkTextPart = (TextPart) chunkPart;

// Find the first text part in existing message to append to
boolean appended = false;
for (int i = 0; i < mergedParts.size(); i++) {
MessagePart existingPart = mergedParts.get(i);
if (existingPart instanceof TextPart) {
TextPart existingTextPart = (TextPart) existingPart;
// Create a new TextPart with combined content
TextPart mergedTextPart =
TextPart.create(existingTextPart.getContent() + chunkTextPart.getContent());
mergedParts.set(i, mergedTextPart);
appended = true;
break;
}
}

// If no existing text part found, add the chunk as a new part
if (!appended) {
mergedParts.add(chunkTextPart);
}
} else {
// For non-text parts, add them as new parts
mergedParts.add(chunkPart);
}
}

// Create new OutputMessage with merged parts, using the chunk's finish reason if available
String finalFinishReason =
chunkMessage.getFinishReason() != null
? chunkMessage.getFinishReason()
: existingMessage.getFinishReason();

OutputMessage mergedMessage =
OutputMessage.create(existingMessage.getRole(), mergedParts, finalFinishReason);

// Replace the message at the specified index
currentMessages.set(index, mergedMessage);

return new AutoValue_OutputMessages(currentMessages);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.instrumentation.api.incubator.semconv.genai.messages;

public enum Role {
SYSTEM("system"),
USER("user"),
ASSISTANT("assistant"),
TOOL("tool"),
DEVELOPER("developer");

private final String value;

public String getValue() {
return value;
}

Role(String value) {
this.value = value;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.instrumentation.api.incubator.semconv.genai.messages;

import com.fasterxml.jackson.annotation.JsonClassDescription;
import com.fasterxml.jackson.annotation.JsonPropertyDescription;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.auto.value.AutoValue;
import java.util.List;

/** Represents the list of system instructions sent to the model. */
@AutoValue
@JsonClassDescription("System instructions")
public abstract class SystemInstructions {

@JsonPropertyDescription("List of message parts that make up the system instructions")
public abstract List<MessagePart> getParts();

public static SystemInstructions create(List<MessagePart> parts) {
return new AutoValue_SystemInstructions(parts);
}

public String toJsonString() {
try {
ObjectMapper mapper = new ObjectMapper();
return mapper.writeValueAsString(this);
} catch (JsonProcessingException e) {
throw new RuntimeException("Failed to serialize SystemInstructions to JSON", e);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.instrumentation.api.incubator.semconv.genai.messages;

import com.fasterxml.jackson.annotation.JsonClassDescription;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonPropertyDescription;
import com.google.auto.value.AutoValue;

/** Represents text content sent to or received from the model. */
@AutoValue
@JsonClassDescription("Text part")
public abstract class TextPart implements MessagePart {

@JsonProperty(required = true, value = "type")
@JsonPropertyDescription("The type of the content captured in this part")
public abstract String getType();

@JsonProperty(required = true, value = "content")
@JsonPropertyDescription("Text content sent to or received from the model")
public abstract String getContent();

public static TextPart create(String content) {
return new AutoValue_TextPart("text", content);
}
}
Loading
Loading