Skip to content

Commit d548421

Browse files
committed
support llm messages with tool calls
1 parent 860fb45 commit d548421

File tree

3 files changed

+73
-44
lines changed

3 files changed

+73
-44
lines changed

dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/Agent.java

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -103,9 +103,8 @@ private enum AgentFeature {
103103
SPAN_ORIGIN(TraceInstrumentationConfig.CODE_ORIGIN_FOR_SPANS_ENABLED, false),
104104
DATA_JOBS(GeneralConfig.DATA_JOBS_ENABLED, false),
105105
AGENTLESS_LOG_SUBMISSION(GeneralConfig.AGENTLESS_LOG_SUBMISSION_ENABLED, false),
106-
LLMOBS(propertyNameToSystemPropertyName(LlmObsConfig.LLMOBS_ENABLED), false),
107-
LLMOBS_AGENTLESS(
108-
propertyNameToSystemPropertyName(LlmObsConfig.LLMOBS_AGENTLESS_ENABLED), false);
106+
LLMOBS(LlmObsConfig.LLMOBS_ENABLED, false),
107+
LLMOBS_AGENTLESS(LlmObsConfig.LLMOBS_AGENTLESS_ENABLED, false);
109108

110109
private final String configKey;
111110
private final String systemProp;

dd-java-agent/agent-llmobs/src/main/java/datadog/trace/llmobs/domain/DDLLMObsSpan.java

Lines changed: 12 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,22 @@
11
package datadog.trace.llmobs.domain;
22

33
import datadog.trace.api.DDSpanTypes;
4+
import datadog.trace.api.llmobs.LLMObs;
45
import datadog.trace.api.llmobs.LLMObsSpan;
56
import datadog.trace.api.llmobs.LLMObsTags;
67
import datadog.trace.bootstrap.instrumentation.api.AgentSpan;
78
import datadog.trace.bootstrap.instrumentation.api.AgentTracer;
89
import datadog.trace.bootstrap.instrumentation.api.Tags;
9-
import java.util.Arrays;
1010
import java.util.Collections;
1111
import java.util.HashMap;
12-
import java.util.HashSet;
1312
import java.util.List;
1413
import java.util.Map;
15-
import java.util.Set;
1614
import javax.annotation.Nonnull;
1715
import org.slf4j.Logger;
1816
import org.slf4j.LoggerFactory;
1917

2018
public class DDLLMObsSpan implements LLMObsSpan {
21-
22-
private enum State {
23-
VALID,
24-
INVALID_IO_MESSAGE_KEY
25-
}
26-
27-
private static final String MESSAGE_KEY_ROLE = "role";
28-
private static final String MESSAGE_KEY_CONTENT = "content";
29-
30-
private static final Set<String> VALID_MESSAGE_KEYS =
31-
new HashSet<>(Arrays.asList(MESSAGE_KEY_ROLE, MESSAGE_KEY_CONTENT));
19+
private static final String LLM_MESSAGE_UNKNOWN_ROLE = "unknown";
3220

3321
// Well known tags for LLM obs will be prefixed with _ml_obs_(tags|metrics).
3422
// Prefix for tags
@@ -92,35 +80,15 @@ public String toString() {
9280
+ this.span.getTag(SPAN_KIND);
9381
}
9482

95-
private static State validateIOMessages(List<Map<String, Object>> messages) {
96-
for (Map<String, Object> message : messages) {
97-
for (String key : message.keySet()) {
98-
if (!VALID_MESSAGE_KEYS.contains(key)) {
99-
return State.INVALID_IO_MESSAGE_KEY;
100-
}
101-
}
102-
}
103-
return State.VALID;
104-
}
105-
10683
@Override
107-
public void annotateIO(
108-
List<Map<String, Object>> inputData, List<Map<String, Object>> outputData) {
84+
public void annotateIO(List<LLMObs.LLMMessage> inputData, List<LLMObs.LLMMessage> outputData) {
10985
if (finished) {
11086
return;
11187
}
11288
if (inputData != null && !inputData.isEmpty()) {
113-
State inputState = validateIOMessages(inputData);
114-
if (validateIOMessages(inputData) != State.VALID) {
115-
LOGGER.debug("malformed/unexpected input message, state={}", inputState);
116-
}
11789
this.span.setTag(INPUT, inputData);
11890
}
11991
if (outputData != null && !outputData.isEmpty()) {
120-
State outputState = validateIOMessages(outputData);
121-
if (validateIOMessages(outputData) != State.VALID) {
122-
LOGGER.debug("malformed/unexpected output message, state={}", outputState);
123-
}
12492
this.span.setTag(OUTPUT, outputData);
12593
}
12694
}
@@ -130,24 +98,31 @@ public void annotateIO(String inputData, String outputData) {
13098
if (finished) {
13199
return;
132100
}
101+
boolean wrongSpanKind = false;
133102
if (inputData != null && !inputData.isEmpty()) {
134103
if (Tags.LLMOBS_LLM_SPAN_KIND.equals(this.spanKind)) {
104+
wrongSpanKind = true;
135105
annotateIO(
136-
Collections.singletonList(Collections.singletonMap(MESSAGE_KEY_CONTENT, inputData)),
106+
Collections.singletonList(LLMObs.LLMMessage.from(LLM_MESSAGE_UNKNOWN_ROLE, inputData)),
137107
null);
138108
} else {
139109
this.span.setTag(INPUT, inputData);
140110
}
141111
}
142112
if (outputData != null && !outputData.isEmpty()) {
143113
if (Tags.LLMOBS_LLM_SPAN_KIND.equals(this.spanKind)) {
114+
wrongSpanKind = true;
144115
annotateIO(
145116
null,
146-
Collections.singletonList(Collections.singletonMap(MESSAGE_KEY_CONTENT, outputData)));
117+
Collections.singletonList(LLMObs.LLMMessage.from(LLM_MESSAGE_UNKNOWN_ROLE, outputData)));
147118
} else {
148119
this.span.setTag(OUTPUT, outputData);
149120
}
150121
}
122+
if (wrongSpanKind) {
123+
LOGGER.warn(
124+
"the span being annotated is an LLM span, it is recommended to use the overload with List<LLMObs.LLMMessage> as arguments");
125+
}
151126
}
152127

153128
@Override

dd-java-agent/agent-llmobs/src/test/groovy/datadog/trace/llmobs/domain/DDLLMObsSpanTest.groovy

Lines changed: 59 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package datadog.trace.llmobs.domain
33
import datadog.trace.agent.tooling.TracerInstaller
44
import datadog.trace.api.DDTags
55
import datadog.trace.api.IdGenerationStrategy
6+
import datadog.trace.api.llmobs.LLMObs
67
import datadog.trace.api.llmobs.LLMObsSpan
78
import datadog.trace.api.llmobs.LLMObsTags
89
import datadog.trace.bootstrap.instrumentation.api.AgentSpan
@@ -199,11 +200,65 @@ class DDLLMObsSpanTest extends DDSpecification{
199200
assert Tags.LLMOBS_LLM_SPAN_KIND.equals(innerSpan.getTag(LLMOBS_TAG_PREFIX + "span.kind"))
200201

201202
assert null == innerSpan.getTag("input")
202-
def expectedInput = Arrays.asList(Maps.of("content", input))
203-
assert expectedInput.equals(innerSpan.getTag(INPUT))
203+
def spanInput = innerSpan.getTag(INPUT)
204+
assert spanInput instanceof List
205+
assert ((List)spanInput).size() == 1
206+
assert spanInput.get(0) instanceof LLMObs.LLMMessage
207+
def expectedInputMsg = LLMObs.LLMMessage.from("unknown", input)
208+
assert expectedInputMsg.getContent().equals(input)
209+
assert expectedInputMsg.getRole().equals("unknown")
210+
assert expectedInputMsg.getToolCalls().equals(null)
211+
204212
assert null == innerSpan.getTag("output")
205-
def expectedOutput = Arrays.asList(Maps.of("content", output))
206-
assert expectedOutput.equals(innerSpan.getTag(OUTPUT))
213+
def spanOutput = innerSpan.getTag(OUTPUT)
214+
assert spanOutput instanceof List
215+
assert ((List)spanOutput).size() == 1
216+
assert spanOutput.get(0) instanceof LLMObs.LLMMessage
217+
def expectedOutputMsg = LLMObs.LLMMessage.from("unknown", output)
218+
assert expectedOutputMsg.getContent().equals(output)
219+
assert expectedOutputMsg.getRole().equals("unknown")
220+
assert expectedOutputMsg.getToolCalls().equals(null)
221+
}
222+
223+
def "test llm span with messages"() {
224+
setup:
225+
def test = givenALLMObsSpan(Tags.LLMOBS_LLM_SPAN_KIND, "test-span")
226+
227+
when:
228+
def inputMsg = LLMObs.LLMMessage.from("user", "input")
229+
def outputMsg = LLMObs.LLMMessage.from("assistant", "output", Arrays.asList(LLMObs.ToolCall.from("weather-tool", "function", "6176241000", Maps.of("location", "paris"))))
230+
// initial set
231+
test.annotateIO(Arrays.asList(inputMsg), Arrays.asList(outputMsg))
232+
233+
then:
234+
def innerSpan = (AgentSpan)test.span
235+
assert Tags.LLMOBS_LLM_SPAN_KIND.equals(innerSpan.getTag(LLMOBS_TAG_PREFIX + "span.kind"))
236+
237+
assert null == innerSpan.getTag("input")
238+
def spanInput = innerSpan.getTag(INPUT)
239+
assert spanInput instanceof List
240+
assert ((List)spanInput).size() == 1
241+
def spanInputMsg = spanInput.get(0)
242+
assert spanInputMsg instanceof LLMObs.LLMMessage
243+
assert spanInputMsg.getContent().equals(inputMsg.getContent())
244+
assert spanInputMsg.getRole().equals("user")
245+
assert spanInputMsg.getToolCalls().equals(null)
246+
247+
assert null == innerSpan.getTag("output")
248+
def spanOutput = innerSpan.getTag(OUTPUT)
249+
assert spanOutput instanceof List
250+
assert ((List)spanOutput).size() == 1
251+
def spanOutputMsg = spanOutput.get(0)
252+
assert spanOutputMsg instanceof LLMObs.LLMMessage
253+
assert spanOutputMsg.getContent().equals(outputMsg.getContent())
254+
assert spanOutputMsg.getRole().equals("assistant")
255+
assert spanOutputMsg.getToolCalls().size() == 1
256+
def toolCall = spanOutputMsg.getToolCalls().get(0)
257+
assert toolCall.getName().equals("weather-tool")
258+
assert toolCall.getType().equals("function")
259+
assert toolCall.getToolID().equals("6176241000")
260+
def expectedToolArgs = Maps.of("location", "paris")
261+
assert toolCall.getArguments().equals(expectedToolArgs)
207262
}
208263

209264
private LLMObsSpan givenALLMObsSpan(String kind, name){

0 commit comments

Comments
 (0)