Skip to content
Merged
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
5 changes: 4 additions & 1 deletion README-zh.md
Original file line number Diff line number Diff line change
Expand Up @@ -429,6 +429,9 @@ loongsuite-instrument \

```bash
export LOONGSUITE_PYTHON_SITE_BOOTSTRAP=True
# 交互式 CLI / 应用可选:不要把通用成功提示写入 stdout。
export LOONGSUITE_PYTHON_SITE_BOOTSTRAP_LOG_SUCCESS=False
export LOONGSUITE_PYTHON_SITE_BOOTSTRAP_STATUS_FILE=/tmp/loongsuite-site-bootstrap-status.json
```

**步骤 4 — 创建 `~/.loongsuite/bootstrap-config.json`**
Expand All @@ -445,7 +448,7 @@ loongsuite-instrument \
}
```

然后执行 `python demo.py`。如需使用 **console** exporter、其他后端、改用 **`loongsuite-instrument`**(而非直接 `python`),或查看完整优先级/边界场景,请阅读 [loongsuite-site-bootstrap/README.md](loongsuite-site-bootstrap/README.md)。
然后执行 `python demo.py`。如需使用 **console** exporter、其他后端、改用 **`loongsuite-instrument`**(而非直接 `python`)、控制成功提示输出,或查看完整优先级/边界场景,请阅读 [loongsuite-site-bootstrap/README.md](loongsuite-site-bootstrap/README.md)。

> **Beta:**Site-bootstrap 会影响其启用环境中的所有 Python 进程,生产环境使用前请先阅读包 README。

Expand Down
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -423,6 +423,9 @@ Run **without** changing codes or bootstrap commands: a **`.pth` hook** loads Lo

```bash
export LOONGSUITE_PYTHON_SITE_BOOTSTRAP=True
# Optional for interactive CLIs/apps: suppress the generic success line on stdout.
export LOONGSUITE_PYTHON_SITE_BOOTSTRAP_LOG_SUCCESS=False
export LOONGSUITE_PYTHON_SITE_BOOTSTRAP_STATUS_FILE=/tmp/loongsuite-site-bootstrap-status.json
```

**Step 4 — Create `~/.loongsuite/bootstrap-config.json`** with the OpenTelemetry environments keys you need.
Expand All @@ -437,7 +440,7 @@ Run **without** changing codes or bootstrap commands: a **`.pth` hook** loads Lo
}
```

Then run `python demo.py`. For **console** exporters, other backends, using **`loongsuite-instrument`** instead of plain `python`, or full precedence / edge cases, see [loongsuite-site-bootstrap/README.md](loongsuite-site-bootstrap/README.md).
Then run `python demo.py`. For **console** exporters, other backends, using **`loongsuite-instrument`** instead of plain `python`, success logging controls, or full precedence / edge cases, see [loongsuite-site-bootstrap/README.md](loongsuite-site-bootstrap/README.md).

> **Beta:** Site-bootstrap affects every Python process in the environment where it is enabled; read the package README before using it in production.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ def on_chat_model_start(
for sub_messages in messages:
for message in sub_messages:
# Cast to Any to avoid type checking issues with LangChain's complex content type
raw_content: Any = message.content # type: ignore[misc]
raw_content: Any = message.content
role = message.type
parts: list[Text] = []

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@
from opentelemetry.util.genai.types import Error, LLMInvocation

from .utils import (
apply_entry_baggage_identity,
convert_agent_response_to_output_messages,
convert_chatresponse_to_output_messages,
create_agent_invocation,
Expand Down Expand Up @@ -182,6 +183,7 @@ def hook(agent_self: Any, kwargs: dict) -> None:

state.react_round += 1
inv = ReactStepInvocation(round=state.react_round)
apply_entry_baggage_identity(inv)
handler.start_react_step(inv, context=state.original_context)
state.active_step = inv
state.pending_acting_count = 0
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,11 @@
from opentelemetry.util.genai.span_utils import _apply_error_attributes
from opentelemetry.util.genai.types import Error

from .utils import (
apply_entry_baggage_identity,
entry_baggage_identity_attributes,
)

logger = logging.getLogger(__name__)


Expand Down Expand Up @@ -407,6 +412,7 @@ async def wrap_tool_call(wrapped, instance, args, kwargs, handler):
tool_description=tool_description,
tool_call_arguments=tool_args,
)
apply_entry_baggage_identity(invocation)

# --- Skill attributes ---
#
Expand Down Expand Up @@ -479,6 +485,7 @@ async def wrap_formatter_format(wrapped, instance, args, kwargs, tracer=None):
try:
# Record only basic information
span.set_attribute("gen_ai.operation.name", "format")
span.set_attributes(entry_baggage_identity_attributes())

# Execute the wrapped async call
result = await wrapped(*args, **kwargs)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,14 @@
from agentscope.model import ChatModelBase, ChatResponse
from pydantic import BaseModel

from opentelemetry import baggage
from opentelemetry.semconv._incubating.attributes import (
gen_ai_attributes as GenAIAttributes,
)
from opentelemetry.util.genai.extended_semconv.gen_ai_extended_attributes import (
GEN_AI_SESSION_ID,
GEN_AI_USER_ID,
)
from opentelemetry.util.genai.extended_types import (
EmbeddingInvocation,
InvokeAgentInvocation,
Expand Down Expand Up @@ -87,6 +92,43 @@ class AgentScopeGenAiProviderName(str, Enum):
]


def _current_baggage_value(key: str) -> str | None:
try:
value = baggage.get_baggage(key)
except Exception:
return None
if value is None:
return None
text = str(value).strip()
return text or None


def entry_baggage_identity_attributes() -> dict[str, str]:
"""Return entry-level identity from current OpenTelemetry Baggage.

QwenPaw opens an Entry span before AgentScope runs and writes
``gen_ai.session.id`` / ``gen_ai.user.id`` into Baggage. AgentScope has
its own ``run_id``; when both instrumentations are active, entry baggage is
the request-level identity and should color downstream spans.
"""
attributes: dict[str, str] = {}
session_id = _current_baggage_value(GEN_AI_SESSION_ID)
user_id = _current_baggage_value(GEN_AI_USER_ID)
if session_id:
attributes[GEN_AI_SESSION_ID] = session_id
if user_id:
attributes[GEN_AI_USER_ID] = user_id
return attributes


def apply_entry_baggage_identity(invocation: Any) -> str | None:
"""Copy entry-level identity baggage onto a GenAI invocation."""
attributes = entry_baggage_identity_attributes()
for key, value in attributes.items():
invocation.attributes.setdefault(key, value)
return attributes.get(GEN_AI_SESSION_ID)


def get_provider_name(chat_model: ChatModelBase) -> str:
"""Parse chat model provider name"""
classname = chat_model.__class__.__name__
Expand Down Expand Up @@ -318,6 +360,9 @@ def create_llm_invocation(
provider=provider_name,
input_messages=input_messages,
)
entry_session_id = apply_entry_baggage_identity(invocation)
if entry_session_id and invocation.conversation_id is None:
invocation.conversation_id = entry_session_id

# Set optional request parameters if present
if call_kwargs.get("max_tokens"):
Expand Down Expand Up @@ -353,6 +398,7 @@ def create_embedding_invocation(
request_model=request_model,
provider=provider_name,
)
apply_entry_baggage_identity(invocation)

# Set encoding formats if present
if call_kwargs.get("encoding_formats"):
Expand Down Expand Up @@ -392,16 +438,18 @@ def create_agent_invocation(
except Exception as e:
logger.debug(f"Error converting agent input messages: {e}")

entry_session_id = _current_baggage_value(GEN_AI_SESSION_ID)
invocation = InvokeAgentInvocation(
provider=provider_name,
agent_name=getattr(reply_instance, "name", "unknown_agent"),
agent_id=getattr(reply_instance, "id", "unknown"),
agent_description=inspect.getdoc(reply_instance.__class__)
or "No description available",
conversation_id=_config.run_id,
conversation_id=entry_session_id or _config.run_id,
request_model=request_model,
input_messages=input_messages,
)
apply_entry_baggage_identity(invocation)

# Set system instruction if available
if hasattr(reply_instance, "sys_prompt") and reply_instance.sys_prompt:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,17 +17,32 @@
Tests for utility functions in opentelemetry.instrumentation.agentscope.utils
"""

from types import SimpleNamespace

from agentscope.message import Msg, ToolResultBlock
from agentscope.tracing._converter import (
_convert_block_to_part as _convert_block_to_part_framework,
)

from opentelemetry import baggage
from opentelemetry import context as otel_context
from opentelemetry.instrumentation.agentscope import utils as utils_module
from opentelemetry.instrumentation.agentscope.utils import (
_convert_block_to_part as _convert_block_to_part_local,
)
from opentelemetry.instrumentation.agentscope.utils import (
apply_entry_baggage_identity,
convert_agentscope_messages_to_genai_format,
create_agent_invocation,
create_embedding_invocation,
create_llm_invocation,
entry_baggage_identity_attributes,
)
from opentelemetry.util.genai.extended_semconv.gen_ai_extended_attributes import (
GEN_AI_SESSION_ID,
GEN_AI_USER_ID,
)
from opentelemetry.util.genai.extended_types import ReactStepInvocation
from opentelemetry.util.genai.types import ToolCallResponse


Expand Down Expand Up @@ -103,3 +118,106 @@ def test_convert_with_framework_converter(self):
part_obj = converted[0].parts[0]
assert isinstance(part_obj, ToolCallResponse)
assert part_obj.response == "framework output"

def test_create_agent_invocation_prefers_entry_baggage_identity(
self, monkeypatch
):
monkeypatch.setattr(
utils_module._config, "run_id", "agentscope-run-id"
)
ctx = baggage.set_baggage(GEN_AI_SESSION_ID, "entry-session")
ctx = baggage.set_baggage(GEN_AI_USER_ID, "entry-user", ctx)
token = otel_context.attach(ctx)
try:
invocation = create_agent_invocation(
SimpleNamespace(
model=None,
name="TestAgent",
id="agent-id",
sys_prompt=None,
),
tuple(),
{},
)
finally:
otel_context.detach(token)

assert invocation.conversation_id == "entry-session"
assert invocation.attributes[GEN_AI_SESSION_ID] == "entry-session"
assert invocation.attributes[GEN_AI_USER_ID] == "entry-user"

def test_entry_baggage_identity_attributes(self):
ctx = baggage.set_baggage(GEN_AI_SESSION_ID, "entry-session")
ctx = baggage.set_baggage(GEN_AI_USER_ID, "entry-user", ctx)
token = otel_context.attach(ctx)
try:
attributes = entry_baggage_identity_attributes()
finally:
otel_context.detach(token)

assert attributes == {
GEN_AI_SESSION_ID: "entry-session",
GEN_AI_USER_ID: "entry-user",
}

def test_create_agent_invocation_falls_back_to_agentscope_run_id(
self, monkeypatch
):
monkeypatch.setattr(
utils_module._config, "run_id", "agentscope-run-id"
)

invocation = create_agent_invocation(
SimpleNamespace(
model=None,
name="TestAgent",
id="agent-id",
sys_prompt=None,
),
tuple(),
{},
)

assert invocation.conversation_id == "agentscope-run-id"
assert GEN_AI_SESSION_ID not in invocation.attributes
assert GEN_AI_USER_ID not in invocation.attributes

def test_model_invocations_copy_entry_baggage_identity(self):
ctx = baggage.set_baggage(GEN_AI_SESSION_ID, "entry-session")
ctx = baggage.set_baggage(GEN_AI_USER_ID, "entry-user", ctx)
token = otel_context.attach(ctx)
try:
llm_invocation = create_llm_invocation(
SimpleNamespace(model_name="qwen-max"),
tuple(),
{},
)
embedding_invocation = create_embedding_invocation(
SimpleNamespace(model_name="text-embedding-v4"),
tuple(),
{},
)
finally:
otel_context.detach(token)

assert llm_invocation.conversation_id == "entry-session"
assert llm_invocation.attributes[GEN_AI_SESSION_ID] == "entry-session"
assert llm_invocation.attributes[GEN_AI_USER_ID] == "entry-user"
assert (
embedding_invocation.attributes[GEN_AI_SESSION_ID]
== "entry-session"
)
assert embedding_invocation.attributes[GEN_AI_USER_ID] == "entry-user"

def test_react_step_invocation_copies_entry_baggage_identity(self):
ctx = baggage.set_baggage(GEN_AI_SESSION_ID, "entry-session")
ctx = baggage.set_baggage(GEN_AI_USER_ID, "entry-user", ctx)
token = otel_context.attach(ctx)
try:
invocation = ReactStepInvocation(round=1)
apply_entry_baggage_identity(invocation)
finally:
otel_context.detach(token)

assert invocation.attributes[GEN_AI_SESSION_ID] == "entry-session"
assert invocation.attributes[GEN_AI_USER_ID] == "entry-user"
Original file line number Diff line number Diff line change
Expand Up @@ -270,7 +270,7 @@ def _handle_llm_start(self, run: Run) -> None:
rd = _RunData(
run_kind="llm",
span=invocation.span,
context=set_span_in_context(invocation.span)
context=otel_context.get_current()
if invocation.span
else None,
invocation=invocation,
Expand Down Expand Up @@ -413,9 +413,7 @@ def _start_agent(self, run: Run) -> None:
rd = _RunData(
run_kind="agent",
span=invocation.span,
context=set_span_in_context(invocation.span)
if invocation.span
else None,
context=otel_context.get_current() if invocation.span else None,
invocation=invocation,
is_langgraph_react=_has_langgraph_react_metadata(run),
)
Expand All @@ -437,7 +435,12 @@ def _start_chain(self, run: Run) -> None:
span.set_attribute(INPUT_VALUE, _safe_json(inputs))

# Attach chain span context so non-LangChain children nest correctly.
ctx = set_span_in_context(span)
current_context = (
parent_ctx
if parent_ctx is not None
else otel_context.get_current()
)
ctx = set_span_in_context(span, current_context)
token = otel_context.attach(ctx)

# Propagate inside_langgraph_react from parent so that
Expand Down Expand Up @@ -576,7 +579,7 @@ def _on_tool_start(self, run: Run) -> None:
rd = _RunData(
run_kind="tool",
span=invocation.span,
context=set_span_in_context(invocation.span)
context=otel_context.get_current()
if invocation.span
else None,
invocation=invocation,
Expand Down Expand Up @@ -634,7 +637,7 @@ def _on_retriever_start(self, run: Run) -> None:
rd = _RunData(
run_kind="retriever",
span=invocation.span,
context=set_span_in_context(invocation.span)
context=otel_context.get_current()
if invocation.span
else None,
invocation=invocation,
Expand Down Expand Up @@ -724,7 +727,7 @@ def _enter_react_step(self, agent_run_id: UUID) -> None:
self._handler.start_react_step(inv, context=agent_rd.original_context)

step_ctx = (
set_span_in_context(inv.span)
otel_context.get_current()
if inv.span
else agent_rd.original_context
)
Expand Down
Loading
Loading