Skip to content
Open
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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Release History

# 1.3.7 (Unreleased)
### Features Added
- Capture the agent's system prompt as the `gen_ai.system_instructions` span attribute in the LangChain instrumentation.

# 1.3.6 (2026-07-22)

### Features Added
Expand Down
19 changes: 17 additions & 2 deletions src/microsoft/opentelemetry/_genai/_langchain/_tracer.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@
GEN_AI_PROVIDER_NAME_KEY,
GEN_AI_REQUEST_CHOICE_COUNT_KEY,
GEN_AI_REQUEST_MODEL_KEY,
GEN_AI_SYSTEM_INSTRUCTIONS_KEY,
GEN_AI_TOOL_DEFINITIONS_KEY,
GEN_AI_USAGE_INPUT_TOKENS_KEY,
GEN_AI_USAGE_OUTPUT_TOKENS_KEY,
Expand All @@ -69,10 +70,10 @@
llm_provider,
metadata,
model_name,
prompts,
_extract_structured_output_messages,
_extract_agent_input_messages,
_extract_agent_output_messages,
_extract_system_instruction,
_output_message_to_input,
_is_structured_output_run,
_seed_initial_messages,
Expand Down Expand Up @@ -260,6 +261,7 @@ def _start_trace(self, run: Run) -> None:
"output_messages": [],
"pending_assistant": None,
"seeded_initial": False,
"system_instruction": None,
"model": None,
"provider": None,
"request_choice_count": None,
Expand Down Expand Up @@ -524,6 +526,10 @@ def _aggregate_into_parent(self, run: Run) -> None: # pylint: disable=too-many-
# (e.g. a routing decision), not a conversational turn, and folding
# it into the transcript pollutes ``gen_ai.input.messages``.
if run_type in ("llm", "chat_model") and not _is_structured_output_run(run):
if not content.get("system_instruction"):
system_instruction = _extract_system_instruction(run.inputs)
if system_instruction:
content["system_instruction"] = system_instruction
# Seed system/user messages from the agent's top-level inputs
# on the first LLM call.
if not content.get("seeded_initial"):
Expand Down Expand Up @@ -634,6 +640,11 @@ def _finalize_agent_span(self, span: Span, run: Run) -> None:

# Set aggregated input/output messages only when content capture is enabled
if _should_capture_content_on_spans(self._enable_sensitive_data):
if system_instruction := content.get("system_instruction"):
span.set_attribute(
GEN_AI_SYSTEM_INSTRUCTIONS_KEY,
safe_json_dumps([asdict(p) for p in system_instruction]),
)
if tool_defs := content.get("tool_definitions"):
span.set_attribute(GEN_AI_TOOL_DEFINITIONS_KEY, tool_defs)
if msgs := content.get("input_messages"):
Expand Down Expand Up @@ -707,12 +718,16 @@ def _update_span(span: Span, run: Run, enable_sensitive_data: bool = False) -> L
GEN_AI_OUTPUT_MESSAGES_KEY,
safe_json_dumps([asdict(m) for m in invocation.output_messages]),
)
if invocation.system_instruction:
span.set_attribute(
GEN_AI_SYSTEM_INSTRUCTIONS_KEY,
safe_json_dumps([asdict(p) for p in invocation.system_instruction]),
)
Comment thread
rads-1996 marked this conversation as resolved.
# Extras not covered by LLMInvocation
span.set_attributes(
dict(
flatten(
chain(
prompts(run.inputs, enable_sensitive_data),
invocation_parameters(run),
function_calls(run.outputs, enable_sensitive_data),
metadata(run),
Expand Down
38 changes: 23 additions & 15 deletions src/microsoft/opentelemetry/_genai/_langchain/_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -263,21 +263,6 @@ def __delitem__(self, key: K) -> None:
LANGCHAIN_THREAD_ID = "thread_id"


@stop_on_exception
def prompts(
inputs: Mapping[str, Any] | None,
enable_sensitive_data: bool = False,
) -> Iterator[tuple[str, list[str]]]:
if not _should_capture_content_on_spans(enable_sensitive_data):
return
if not inputs:
return
if not isinstance(inputs, Mapping):
return
if p := inputs.get("prompts"):
yield GEN_AI_SYSTEM_INSTRUCTIONS_KEY, p


@stop_on_exception
def input_messages(
inputs: Mapping[str, Any] | None,
Expand Down Expand Up @@ -1323,6 +1308,25 @@ def _extract_system_instruction(inputs: Mapping[str, Any] | None) -> list[Text]:
return [Text(content=str(item)) for item in p if item]
if isinstance(p, str):
return [Text(content=p)]
return []
# Messages path: only reached when there are no prompts.
multiple_messages = inputs.get("messages")
if multiple_messages and isinstance(multiple_messages, Iterable):
if isinstance(multiple_messages, list) and multiple_messages:
first_item = multiple_messages[0]
first_messages = first_item if isinstance(first_item, list) else multiple_messages
else:
first_messages = next(iter(multiple_messages), None) # type: ignore[arg-type]
if first_messages is not None:
if not isinstance(first_messages, list):
first_messages = [first_messages]
results: list[Text] = []
for msg in first_messages:
if _normalize_role(_langchain_role(msg)) == "system":
content = _langchain_content(msg)
if content:
results.append(Text(content=content))
return results
return []


Expand All @@ -1332,6 +1336,8 @@ def _extract_structured_input_messages(
"""Convert LangChain input messages to OTel ``InputMessage`` list."""
if not inputs or not isinstance(inputs, Mapping):
return []

route_system_out = not inputs.get("prompts")
multiple_messages = inputs.get("messages")
if multiple_messages and isinstance(multiple_messages, Iterable):
first_messages = next(iter(multiple_messages), None)
Expand All @@ -1342,6 +1348,8 @@ def _extract_structured_input_messages(
results: list[InputMessage] = []
for msg in first_messages:
role = _normalize_role(_langchain_role(msg))
if route_system_out and role == "system":
continue
parts: list[Any] = []
tool_responses = _langchain_tool_responses(msg)
if tool_responses:
Expand Down
Loading
Loading