Skip to content

Commit 14072b4

Browse files
fix: properly check that only the last message is kept when doing multi-turn conversations (#135)
* fix: properly check that only the last message is kept when doing multi-turn conversations * fix: manual instrumentation test fixes
1 parent 9452281 commit 14072b4

5 files changed

Lines changed: 364 additions & 132 deletions

File tree

src/runner/templates/agents/node/manual/template.njk

Lines changed: 39 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -45,46 +45,50 @@ function trimMessages(messages, maxSize = 10000) {
4545
}
4646

4747
function buildSentryMessages(messages) {
48-
const sentryMessages = [];
49-
const legacyMessages = [];
5048
let systemInstructions = null;
5149

5250
for (const msg of messages) {
53-
const { role, content } = msg;
54-
55-
if (role === "system") {
56-
systemInstructions = typeof content === "string" ? content : JSON.stringify(content);
51+
if (msg.role === "system") {
52+
systemInstructions =
53+
typeof msg.content === "string" ? msg.content : JSON.stringify(msg.content);
5754
}
55+
}
5856

59-
const parts = [];
60-
if (typeof content === "string") {
61-
parts.push({ type: "text", text: content });
62-
} else if (Array.isArray(content)) {
63-
for (const part of content) {
64-
if (part.type === "text") {
65-
parts.push({ type: "text", text: part.text });
66-
} else if (part.type === "image") {
67-
parts.push({ type: "text", text: "[Blob substitute]" });
68-
}
69-
}
70-
}
57+
const lastMessage = messages[messages.length - 1];
58+
if (!lastMessage) {
59+
return { sentryMessages: [], legacyMessages: [], systemInstructions };
60+
}
7161

72-
sentryMessages.push({
73-
role,
74-
content: typeof content === "string" ? content : "[multimodal]",
75-
parts,
76-
});
77-
78-
if (Array.isArray(content)) {
79-
const redacted = content.map((part) =>
80-
part.type === "image" ? { type: "text", text: "[Blob substitute]" } : part
81-
);
82-
legacyMessages.push({ role, content: redacted });
83-
} else {
84-
legacyMessages.push({ role, content });
62+
const { role, content } = lastMessage;
63+
64+
const parts = [];
65+
if (typeof content === "string") {
66+
parts.push({ type: "text", text: content });
67+
} else if (Array.isArray(content)) {
68+
for (const part of content) {
69+
if (part.type === "text") {
70+
parts.push({ type: "text", text: part.text });
71+
} else if (part.type === "image") {
72+
parts.push({ type: "text", text: "[Blob substitute]" });
73+
}
8574
}
8675
}
8776

77+
const sentryMessages = [{
78+
role,
79+
content: typeof content === "string" ? content : "[multimodal]",
80+
parts,
81+
}];
82+
83+
const legacyMessages = [Array.isArray(content)
84+
? {
85+
role,
86+
content: content.map((part) =>
87+
part.type === "image" ? { type: "text", text: "[Blob substitute]" } : part,
88+
),
89+
}
90+
: { role, content }];
91+
8892
return { sentryMessages, legacyMessages, systemInstructions };
8993
}
9094

@@ -150,6 +154,10 @@ const TOOLS = {{ agent.tools | dump }};
150154
agentSpan.setAttribute("gen_ai.usage.output_tokens", outputTokens{{ loop.index }});
151155
agentSpan.setAttribute("gen_ai.usage.output_tokens.reasoning", Math.max(0, Math.floor(outputTokens{{ loop.index }} / 4)));
152156
agentSpan.setAttribute("gen_ai.usage.total_tokens", totalTokens{{ loop.index }});
157+
agentSpan.setAttribute("gen_ai.input.messages", otelJson{{ loop.index }});
158+
if (system{{ loop.index }} !== null) {
159+
agentSpan.setAttribute("gen_ai.system_instructions", system{{ loop.index }});
160+
}
153161

154162
// Chat span (child of agent)
155163
const chatDesc{{ loop.index }} = `chat ${model{{ loop.index }}}`;

src/runner/templates/agents/python/manual/template.njk

Lines changed: 38 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -48,46 +48,47 @@ def trim_messages(messages, max_size=10000):
4848

4949

5050
def build_sentry_messages(messages):
51-
"""Build gen_ai.input.messages and gen_ai.request.messages from raw messages."""
52-
sentry_messages = []
53-
legacy_messages = []
51+
"""Build gen_ai.input.messages and gen_ai.request.messages from the last raw message."""
5452
system_instructions = None
5553

5654
for msg in messages:
57-
role = msg["role"]
5855
content = msg.get("content", "")
59-
60-
if role == "system":
56+
if msg["role"] == "system":
6157
system_instructions = content if isinstance(content, str) else json.dumps(content)
6258

63-
# New OTel format with parts
64-
parts = []
65-
if isinstance(content, str):
66-
parts.append({"type": "text", "text": content})
67-
elif isinstance(content, list):
68-
for part in content:
69-
if part.get("type") == "text":
70-
parts.append({"type": "text", "text": part["text"]})
71-
elif part.get("type") == "image":
72-
parts.append({"type": "text", "text": "[Blob substitute]"})
73-
74-
sentry_messages.append({
75-
"role": role,
76-
"content": content if isinstance(content, str) else "[multimodal]",
77-
"parts": parts,
78-
})
79-
80-
# Legacy format with binary redaction
81-
if isinstance(content, list):
82-
redacted = []
83-
for part in content:
84-
if part.get("type") == "text":
85-
redacted.append(part)
86-
elif part.get("type") == "image":
87-
redacted.append({"type": "text", "text": "[Blob substitute]"})
88-
legacy_messages.append({"role": role, "content": redacted})
89-
else:
90-
legacy_messages.append({"role": role, "content": content})
59+
if not messages:
60+
return [], [], system_instructions
61+
62+
last_message = messages[-1]
63+
role = last_message["role"]
64+
content = last_message.get("content", "")
65+
66+
parts = []
67+
if isinstance(content, str):
68+
parts.append({"type": "text", "text": content})
69+
elif isinstance(content, list):
70+
for part in content:
71+
if part.get("type") == "text":
72+
parts.append({"type": "text", "text": part["text"]})
73+
elif part.get("type") == "image":
74+
parts.append({"type": "text", "text": "[Blob substitute]"})
75+
76+
sentry_messages = [{
77+
"role": role,
78+
"content": content if isinstance(content, str) else "[multimodal]",
79+
"parts": parts,
80+
}]
81+
82+
if isinstance(content, list):
83+
redacted = []
84+
for part in content:
85+
if part.get("type") == "text":
86+
redacted.append(part)
87+
elif part.get("type") == "image":
88+
redacted.append({"type": "text", "text": "[Blob substitute]"})
89+
legacy_messages = [{"role": role, "content": redacted}]
90+
else:
91+
legacy_messages = [{"role": role, "content": content}]
9192

9293
return sentry_messages, legacy_messages, system_instructions
9394

@@ -151,6 +152,9 @@ TOOLS = {{ agent.tools | dump }}
151152
agent_span.set_data("gen_ai.usage.output_tokens", output_tokens)
152153
agent_span.set_data("gen_ai.usage.output_tokens.reasoning", max(0, output_tokens // 4))
153154
agent_span.set_data("gen_ai.usage.total_tokens", total_tokens)
155+
agent_span.set_data("gen_ai.input.messages", otel_messages_json)
156+
if system_instructions is not None:
157+
agent_span.set_data("gen_ai.system_instructions", system_instructions)
154158

155159
# Chat span (child of agent)
156160
chat_desc = f"chat {model}"

src/runner/templates/llm/node/manual/template.njk

Lines changed: 35 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -45,48 +45,50 @@ function trimMessages(messages, maxSize = 10000) {
4545
}
4646

4747
function buildSentryMessages(messages) {
48-
const sentryMessages = [];
49-
const legacyMessages = [];
5048
let systemInstructions = null;
5149

5250
for (const msg of messages) {
53-
const { role, content } = msg;
54-
55-
if (role === "system") {
56-
systemInstructions = typeof content === "string" ? content : JSON.stringify(content);
51+
if (msg.role === "system") {
52+
systemInstructions =
53+
typeof msg.content === "string" ? msg.content : JSON.stringify(msg.content);
5754
}
55+
}
5856

59-
// New OTel format with parts
60-
const parts = [];
61-
if (typeof content === "string") {
62-
parts.push({ type: "text", text: content });
63-
} else if (Array.isArray(content)) {
64-
for (const part of content) {
65-
if (part.type === "text") {
66-
parts.push({ type: "text", text: part.text });
67-
} else if (part.type === "image") {
68-
parts.push({ type: "text", text: "[Blob substitute]" });
69-
}
70-
}
71-
}
57+
const lastMessage = messages[messages.length - 1];
58+
if (!lastMessage) {
59+
return { sentryMessages: [], legacyMessages: [], systemInstructions };
60+
}
7261

73-
sentryMessages.push({
74-
role,
75-
content: typeof content === "string" ? content : "[multimodal]",
76-
parts,
77-
});
78-
79-
// Legacy format with binary redaction
80-
if (Array.isArray(content)) {
81-
const redacted = content.map((part) =>
82-
part.type === "image" ? { type: "text", text: "[Blob substitute]" } : part
83-
);
84-
legacyMessages.push({ role, content: redacted });
85-
} else {
86-
legacyMessages.push({ role, content });
62+
const { role, content } = lastMessage;
63+
64+
const parts = [];
65+
if (typeof content === "string") {
66+
parts.push({ type: "text", text: content });
67+
} else if (Array.isArray(content)) {
68+
for (const part of content) {
69+
if (part.type === "text") {
70+
parts.push({ type: "text", text: part.text });
71+
} else if (part.type === "image") {
72+
parts.push({ type: "text", text: "[Blob substitute]" });
73+
}
8774
}
8875
}
8976

77+
const sentryMessages = [{
78+
role,
79+
content: typeof content === "string" ? content : "[multimodal]",
80+
parts,
81+
}];
82+
83+
const legacyMessages = [Array.isArray(content)
84+
? {
85+
role,
86+
content: content.map((part) =>
87+
part.type === "image" ? { type: "text", text: "[Blob substitute]" } : part,
88+
),
89+
}
90+
: { role, content }];
91+
9092
return { sentryMessages, legacyMessages, systemInstructions };
9193
}
9294
{% endblock %}

src/runner/templates/llm/python/manual/template.njk

Lines changed: 35 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -44,46 +44,47 @@ def trim_messages(messages, max_size=10000):
4444

4545

4646
def build_sentry_messages(messages):
47-
"""Build gen_ai.input.messages and gen_ai.request.messages from raw messages."""
48-
sentry_messages = []
49-
legacy_messages = []
47+
"""Build gen_ai.input.messages and gen_ai.request.messages from the last raw message."""
5048
system_instructions = None
5149

5250
for msg in messages:
53-
role = msg["role"]
5451
content = msg.get("content", "")
55-
56-
if role == "system":
52+
if msg["role"] == "system":
5753
system_instructions = content if isinstance(content, str) else json.dumps(content)
5854

59-
# New OTel format with parts
60-
parts = []
61-
if isinstance(content, str):
62-
parts.append({"type": "text", "text": content})
63-
elif isinstance(content, list):
64-
for part in content:
65-
if part.get("type") == "text":
66-
parts.append({"type": "text", "text": part["text"]})
67-
elif part.get("type") == "image":
68-
parts.append({"type": "text", "text": "[Blob substitute]"})
69-
70-
sentry_messages.append({
71-
"role": role,
72-
"content": content if isinstance(content, str) else "[multimodal]",
73-
"parts": parts,
74-
})
75-
76-
# Legacy format with binary redaction
77-
if isinstance(content, list):
78-
redacted = []
79-
for part in content:
80-
if part.get("type") == "text":
81-
redacted.append(part)
82-
elif part.get("type") == "image":
83-
redacted.append({"type": "text", "text": "[Blob substitute]"})
84-
legacy_messages.append({"role": role, "content": redacted})
85-
else:
86-
legacy_messages.append({"role": role, "content": content})
55+
if not messages:
56+
return [], [], system_instructions
57+
58+
last_message = messages[-1]
59+
role = last_message["role"]
60+
content = last_message.get("content", "")
61+
62+
parts = []
63+
if isinstance(content, str):
64+
parts.append({"type": "text", "text": content})
65+
elif isinstance(content, list):
66+
for part in content:
67+
if part.get("type") == "text":
68+
parts.append({"type": "text", "text": part["text"]})
69+
elif part.get("type") == "image":
70+
parts.append({"type": "text", "text": "[Blob substitute]"})
71+
72+
sentry_messages = [{
73+
"role": role,
74+
"content": content if isinstance(content, str) else "[multimodal]",
75+
"parts": parts,
76+
}]
77+
78+
if isinstance(content, list):
79+
redacted = []
80+
for part in content:
81+
if part.get("type") == "text":
82+
redacted.append(part)
83+
elif part.get("type") == "image":
84+
redacted.append({"type": "text", "text": "[Blob substitute]"})
85+
legacy_messages = [{"role": role, "content": redacted}]
86+
else:
87+
legacy_messages = [{"role": role, "content": content}]
8788

8889
return sentry_messages, legacy_messages, system_instructions
8990
{% endblock %}

0 commit comments

Comments
 (0)