Skip to content

Commit 085b496

Browse files
feat(openai-agents): Set system instruction attribute on gen_ai.chat spans
1 parent 87dd8fe commit 085b496

5 files changed

Lines changed: 274 additions & 98 deletions

File tree

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
from typing import TYPE_CHECKING
2+
3+
if TYPE_CHECKING:
4+
from typing import Iterable
5+
6+
from sentry_sdk._types import TextPart
7+
8+
from openai.types.chat import (
9+
ChatCompletionMessageParam,
10+
ChatCompletionSystemMessageParam,
11+
)
12+
13+
14+
def _is_system_instruction(message: "ChatCompletionMessageParam") -> bool:
15+
return isinstance(message, dict) and message.get("role") == "system"
16+
17+
18+
def _get_system_instructions(
19+
messages: "Iterable[ChatCompletionMessageParam]",
20+
) -> "list[ChatCompletionSystemMessageParam]":
21+
system_instructions = []
22+
23+
for message in messages:
24+
if _is_system_instruction(message):
25+
system_instructions.append(message)
26+
27+
return system_instructions
28+
29+
30+
def _transform_system_instructions(
31+
system_instructions: "list[ChatCompletionSystemMessageParam]",
32+
) -> "list[TextPart]":
33+
instruction_text_parts: "list[TextPart]" = []
34+
35+
for instruction in system_instructions:
36+
if not isinstance(instruction, dict):
37+
continue
38+
39+
content = instruction.get("content")
40+
41+
if isinstance(content, str):
42+
instruction_text_parts.append({"type": "text", "content": content})
43+
44+
elif isinstance(content, list):
45+
for part in content:
46+
if isinstance(part, dict) and part.get("type") == "text":
47+
text = part.get("text", "")
48+
if text:
49+
instruction_text_parts.append({"type": "text", "content": text})
50+
51+
return instruction_text_parts
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
from typing import TYPE_CHECKING
2+
3+
if TYPE_CHECKING:
4+
from typing import Union
5+
6+
from openai.types.responses import ResponseInputParam, ResponseInputItemParam
7+
8+
9+
def _is_system_instruction(message: "ResponseInputItemParam") -> bool:
10+
return (
11+
isinstance(message, dict)
12+
and message.get("type") == "message"
13+
and message.get("role") == "system"
14+
)
15+
16+
17+
def _get_system_instructions(
18+
messages: "Union[str, ResponseInputParam]",
19+
) -> "list[ResponseInputItemParam]":
20+
if isinstance(messages, str):
21+
return []
22+
23+
system_instructions = []
24+
25+
for message in messages:
26+
if _is_system_instruction(message):
27+
system_instructions.append(message)
28+
29+
return system_instructions

sentry_sdk/integrations/openai.py

Lines changed: 10 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,15 @@
99
normalize_message_roles,
1010
truncate_and_annotate_messages,
1111
)
12+
from sentry_sdk.ai._opanai_completions_api import (
13+
_is_system_instruction as _is_system_instruction_completions,
14+
_get_system_instructions as _get_system_instructions_completions,
15+
_transform_system_instructions,
16+
)
17+
from sentry_sdk.ai._openai_responses_api import (
18+
_is_system_instruction as _is_system_instruction_responses,
19+
_get_system_instructions as _get_system_instructions_responses,
20+
)
1221
from sentry_sdk.consts import SPANDATA
1322
from sentry_sdk.integrations import DidNotEnable, Integration
1423
from sentry_sdk.scope import should_send_default_pii
@@ -36,7 +45,7 @@
3645
from sentry_sdk.tracing import Span
3746
from sentry_sdk._types import TextPart
3847

39-
from openai.types.responses import ResponseInputParam, ResponseInputItemParam
48+
from openai.types.responses import ResponseInputParam
4049
from openai import Omit
4150

4251
try:
@@ -199,69 +208,6 @@ def _calculate_token_usage(
199208
)
200209

201210

202-
def _is_system_instruction_completions(message: "ChatCompletionMessageParam") -> bool:
203-
return isinstance(message, dict) and message.get("role") == "system"
204-
205-
206-
def _get_system_instructions_completions(
207-
messages: "Iterable[ChatCompletionMessageParam]",
208-
) -> "list[ChatCompletionSystemMessageParam]":
209-
system_instructions = []
210-
211-
for message in messages:
212-
if _is_system_instruction_completions(message):
213-
system_instructions.append(message)
214-
215-
return system_instructions
216-
217-
218-
def _is_system_instruction_responses(message: "ResponseInputItemParam") -> bool:
219-
return (
220-
isinstance(message, dict)
221-
and message.get("type") == "message"
222-
and message.get("role") == "system"
223-
)
224-
225-
226-
def _get_system_instructions_responses(
227-
messages: "Union[str, ResponseInputParam]",
228-
) -> "list[ResponseInputItemParam]":
229-
if isinstance(messages, str):
230-
return []
231-
232-
system_instructions = []
233-
234-
for message in messages:
235-
if _is_system_instruction_responses(message):
236-
system_instructions.append(message)
237-
238-
return system_instructions
239-
240-
241-
def _transform_system_instructions(
242-
system_instructions: "list[ChatCompletionSystemMessageParam]",
243-
) -> "list[TextPart]":
244-
instruction_text_parts: "list[TextPart]" = []
245-
246-
for instruction in system_instructions:
247-
if not isinstance(instruction, dict):
248-
continue
249-
250-
content = instruction.get("content")
251-
252-
if isinstance(content, str):
253-
instruction_text_parts.append({"type": "text", "content": content})
254-
255-
elif isinstance(content, list):
256-
for part in content:
257-
if isinstance(part, dict) and part.get("type") == "text":
258-
text = part.get("text", "")
259-
if text:
260-
instruction_text_parts.append({"type": "text", "content": text})
261-
262-
return instruction_text_parts
263-
264-
265211
def _get_input_messages(
266212
kwargs: "dict[str, Any]",
267213
) -> "Optional[Union[Iterable[Any], list[str]]]":

sentry_sdk/integrations/openai_agents/utils.py

Lines changed: 35 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,20 @@
1111
from sentry_sdk.scope import should_send_default_pii
1212
from sentry_sdk.tracing_utils import set_span_errored
1313
from sentry_sdk.utils import event_from_exception, safe_serialize
14+
from sentry_sdk.ai._opanai_completions_api import _transform_system_instructions
15+
from sentry_sdk.ai._openai_responses_api import (
16+
_is_system_instruction,
17+
_get_system_instructions,
18+
)
1419

1520
from typing import TYPE_CHECKING
1621

1722
if TYPE_CHECKING:
1823
from typing import Any
19-
from agents import Usage
24+
from agents import Usage, TResponseInputItem
2025

2126
from sentry_sdk.tracing import Span
27+
from sentry_sdk._types import TextPart
2228

2329
try:
2430
import agents
@@ -115,16 +121,36 @@ def _set_input_data(
115121
return
116122
request_messages = []
117123

118-
system_instructions = get_response_kwargs.get("system_instructions")
119-
if system_instructions:
120-
request_messages.append(
121-
{
122-
"role": GEN_AI_ALLOWED_MESSAGE_ROLES.SYSTEM,
123-
"content": [{"type": "text", "text": system_instructions}],
124-
}
124+
messages: "str | list[TResponseInputItem]" = get_response_kwargs.get("input", [])
125+
126+
explicit_instructions = get_response_kwargs.get("system_instructions")
127+
system_instructions = _get_system_instructions(messages)
128+
129+
if system_instructions is not None or len(system_instructions) > 0:
130+
instructions_text_parts: "list[TextPart]" = []
131+
if explicit_instructions is not None:
132+
instructions_text_parts.append(
133+
{
134+
"type": "text",
135+
"content": explicit_instructions,
136+
}
137+
)
138+
139+
# Deliberate use of function accepting completions API type because
140+
# of shared structure FOR THIS PURPOSE ONLY.
141+
instructions_text_parts += _transform_system_instructions(system_instructions)
142+
143+
set_data_normalized(
144+
span,
145+
SPANDATA.GEN_AI_SYSTEM_INSTRUCTIONS,
146+
instructions_text_parts,
147+
unpack=False,
125148
)
126149

127-
for message in get_response_kwargs.get("input", []):
150+
non_system_messages = [
151+
message for message in messages if not _is_system_instruction(message)
152+
]
153+
for message in non_system_messages:
128154
if "role" in message:
129155
normalized_role = normalize_message_role(message.get("role"))
130156
content = message.get("content")

0 commit comments

Comments
 (0)