Skip to content

Commit 33fb2d6

Browse files
authored
feat: Align retrieval spans with OpenTelemetry semantic conventions (#145)
1 parent 9db22cc commit 33fb2d6

File tree

14 files changed

+375
-154
lines changed

14 files changed

+375
-154
lines changed

instrumentation-loongsuite/loongsuite-instrumentation-langchain/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,7 @@ loongsuite-instrument <your_run_command>
129129
| Agent | `AGENT` | `gen_ai.operation.name=invoke_agent` |
130130
| ReAct Step | `STEP` | `gen_ai.operation.name=react`, `gen_ai.react.round`, `gen_ai.react.finish_reason` |
131131
| Tool | `TOOL` | `gen_ai.operation.name=execute_tool` |
132-
| Retriever | `RETRIEVER` | `gen_ai.operation.name=retrieve_documents` |
132+
| Retriever | `RETRIEVER` | `gen_ai.operation.name=retrieval` |
133133

134134
ReAct Step spans are created for each Reasoning-Acting iteration, with the hierarchy: Agent > ReAct Step > LLM/Tool. Supported agent types:
135135

instrumentation-loongsuite/loongsuite-instrumentation-langchain/src/opentelemetry/instrumentation/langchain/internal/_tracer.py

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333
* **Chain (Agent)** → ``handler.start_invoke_agent`` / …
3434
* **Chain (generic)** → direct span creation (no ``util-genai``)
3535
* **Tool** → ``handler.start_execute_tool`` / …
36-
* **Retriever** → ``handler.start_retrieve`` / …
36+
* **Retriever** → ``handler.start_retrieval`` / …
3737
"""
3838

3939
from __future__ import annotations
@@ -52,6 +52,7 @@
5252
from opentelemetry.context import Context
5353
from opentelemetry.instrumentation.langchain.internal._utils import (
5454
LANGGRAPH_REACT_STEP_NODE,
55+
_documents_to_retrieval_documents,
5556
_extract_finish_reasons,
5657
_extract_invocation_params,
5758
_extract_llm_input_messages,
@@ -84,7 +85,7 @@
8485
from opentelemetry.util.genai.extended_types import (
8586
ExecuteToolInvocation,
8687
InvokeAgentInvocation,
87-
RetrieveInvocation,
88+
RetrievalInvocation,
8889
)
8990
from opentelemetry.util.genai.handler import _safe_detach
9091
from opentelemetry.util.genai.types import (
@@ -626,8 +627,8 @@ def _on_retriever_start(self, run: Run) -> None:
626627
inputs = getattr(run, "inputs", None) or {}
627628
query = inputs.get("query") or ""
628629

629-
invocation = RetrieveInvocation(query=query)
630-
self._handler.start_retrieve(invocation, context=parent_ctx)
630+
invocation = RetrievalInvocation(query=query)
631+
self._handler.start_retrieval(invocation, context=parent_ctx)
631632
rd = _RunData(
632633
run_kind="retriever",
633634
span=invocation.span,
@@ -647,12 +648,12 @@ def _on_retriever_end(self, run: Run) -> None:
647648
if rd is None or rd.run_kind != "retriever":
648649
return
649650
try:
650-
inv: RetrieveInvocation = rd.invocation
651+
inv: RetrievalInvocation = rd.invocation
651652
outputs = getattr(run, "outputs", None) or {}
652653
documents = outputs.get("documents") or []
653654
if documents:
654-
inv.documents = _safe_json(documents)
655-
self._handler.stop_retrieve(inv)
655+
inv.documents = _documents_to_retrieval_documents(documents)
656+
self._handler.stop_retrieval(inv)
656657
except Exception:
657658
logger.debug("Failed to stop Retriever span", exc_info=True)
658659

@@ -663,7 +664,7 @@ def _on_retriever_error(self, run: Run) -> None:
663664
return
664665
try:
665666
err_str = getattr(run, "error", None) or "Unknown error"
666-
self._handler.fail_retrieve(
667+
self._handler.fail_retrieval(
667668
rd.invocation,
668669
Error(message=str(err_str), type=Exception),
669670
)

instrumentation-loongsuite/loongsuite-instrumentation-langchain/src/opentelemetry/instrumentation/langchain/internal/_utils.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import logging
1919
from typing import Any
2020

21+
from opentelemetry.util.genai.extended_types import RetrievalDocument
2122
from opentelemetry.util.genai.types import (
2223
FunctionToolDefinition,
2324
InputMessage,
@@ -405,6 +406,49 @@ def _extract_response_model(run: Any) -> str | None:
405406
return llm_output.get("model_name") or llm_output.get("model")
406407

407408

409+
# ---------------------------------------------------------------------------
410+
# Retriever document conversion
411+
# ---------------------------------------------------------------------------
412+
413+
414+
def _documents_to_retrieval_documents(documents: Any) -> list:
415+
"""Convert retriever output documents to List[RetrievalDocument].
416+
417+
Accepts LangChain Document objects (page_content, metadata) or similar.
418+
Extracts id from doc.id, metadata.id, metadata.doc_id, metadata.document_id.
419+
Extracts score from metadata.score, metadata.relevance_score, metadata.similarity_score.
420+
"""
421+
422+
result = []
423+
if not documents:
424+
return result
425+
for doc in documents:
426+
meta = getattr(doc, "metadata", None) or {}
427+
doc_id = (
428+
getattr(doc, "id", None)
429+
or meta.get("id")
430+
or meta.get("doc_id")
431+
or meta.get("document_id")
432+
)
433+
score = (
434+
meta.get("score")
435+
or meta.get("relevance_score")
436+
or meta.get("similarity_score")
437+
)
438+
content = getattr(doc, "page_content", None) or getattr(
439+
doc, "content", None
440+
)
441+
result.append(
442+
RetrievalDocument(
443+
id=doc_id,
444+
score=score,
445+
content=content,
446+
metadata=meta if meta else None,
447+
)
448+
)
449+
return result
450+
451+
408452
# ---------------------------------------------------------------------------
409453
# JSON serialisation helper
410454
# ---------------------------------------------------------------------------

instrumentation-loongsuite/loongsuite-instrumentation-langchain/src/opentelemetry/instrumentation/langchain/internal/semconv.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
)
2525
from opentelemetry.util.genai._extended_semconv.gen_ai_extended_attributes import ( # noqa: E501
2626
GEN_AI_RETRIEVAL_DOCUMENTS,
27-
GEN_AI_RETRIEVAL_QUERY,
27+
GEN_AI_RETRIEVAL_QUERY_TEXT,
2828
GEN_AI_SPAN_KIND,
2929
GEN_AI_TOOL_CALL_ARGUMENTS,
3030
GEN_AI_TOOL_CALL_RESULT,
@@ -38,7 +38,7 @@
3838
"GEN_AI_OPERATION_NAME",
3939
"GEN_AI_TOOL_CALL_ID",
4040
"GEN_AI_RETRIEVAL_DOCUMENTS",
41-
"GEN_AI_RETRIEVAL_QUERY",
41+
"GEN_AI_RETRIEVAL_QUERY_TEXT",
4242
"GEN_AI_SPAN_KIND",
4343
"GEN_AI_TOOL_CALL_ARGUMENTS",
4444
"GEN_AI_TOOL_CALL_RESULT",

instrumentation-loongsuite/loongsuite-instrumentation-langchain/tests/test_langchain_instrumentor.py

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@
4141
from opentelemetry.instrumentation.langchain.internal.semconv import (
4242
GEN_AI_OPERATION_NAME,
4343
GEN_AI_RETRIEVAL_DOCUMENTS,
44-
GEN_AI_RETRIEVAL_QUERY,
44+
GEN_AI_RETRIEVAL_QUERY_TEXT,
4545
GEN_AI_SPAN_KIND,
4646
INPUT_VALUE,
4747
OUTPUT_VALUE,
@@ -250,14 +250,21 @@ def test_retrieval_qa_chain_spans(
250250
assert sd_span.status.status_code == StatusCode.ERROR
251251
assert not sd_attrs or set(sd_attrs.keys()) <= {"metadata"}
252252

253-
# Retriever span: name is "retrieve_documents"
254-
retriever_span = spans_by_name.get("retrieve_documents")
253+
# Retriever span: name is "retrieval" (or "retrieval {data_source_id}" when set)
254+
retriever_span = spans_by_name.get("retrieval") or next(
255+
(
256+
s
257+
for s in span_exporter.get_finished_spans()
258+
if s.name.startswith("retrieval")
259+
),
260+
None,
261+
)
255262
assert retriever_span is not None
256263
assert retriever_span.parent is not None
257264
assert retriever_span.parent.span_id == rqa_span.context.span_id
258265
retriever_attrs = dict(retriever_span.attributes or {})
259266
assert retriever_attrs.pop(GEN_AI_SPAN_KIND, None) == "RETRIEVER"
260-
assert retriever_attrs.pop(GEN_AI_RETRIEVAL_QUERY, None) == question
267+
assert retriever_attrs.pop(GEN_AI_RETRIEVAL_QUERY_TEXT, None) == question
261268
docs_val = retriever_attrs.pop(GEN_AI_RETRIEVAL_DOCUMENTS, None)
262269
assert docs_val is not None
263270
for text in documents:

instrumentation-loongsuite/loongsuite-instrumentation-langchain/tests/test_retriever_spans.py

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323

2424
from opentelemetry.instrumentation.langchain.internal.semconv import (
2525
GEN_AI_RETRIEVAL_DOCUMENTS,
26-
GEN_AI_RETRIEVAL_QUERY,
26+
GEN_AI_RETRIEVAL_QUERY_TEXT,
2727
)
2828
from opentelemetry.trace import StatusCode
2929

@@ -61,7 +61,7 @@ def _get_relevant_documents(
6161

6262
def _find_retriever_spans(span_exporter):
6363
spans = span_exporter.get_finished_spans()
64-
return [s for s in spans if "retrieve" in s.name.lower()]
64+
return [s for s in spans if "retrieval" in s.name.lower()]
6565

6666

6767
class TestRetrieverSpanCreation:
@@ -96,9 +96,9 @@ def test_retrieval_query_captured(self, instrument, span_exporter):
9696
assert len(retriever_spans) >= 1
9797
attrs = dict(retriever_spans[0].attributes)
9898

99-
query_val = attrs.get(GEN_AI_RETRIEVAL_QUERY, "")
99+
query_val = attrs.get(GEN_AI_RETRIEVAL_QUERY_TEXT, "")
100100
assert "machine learning basics" in query_val, (
101-
f"Expected 'machine learning basics' in retrieval.query, got: {query_val}"
101+
f"Expected 'machine learning basics' in retrieval.query.text, got: {query_val}"
102102
)
103103

104104
def test_retrieval_documents_captured(self, instrument, span_exporter):
@@ -117,17 +117,19 @@ def test_retrieval_documents_captured(self, instrument, span_exporter):
117117
def test_no_content_when_disabled(
118118
self, instrument_no_content, span_exporter
119119
):
120-
"""When content capture is disabled, query and documents should NOT appear."""
120+
"""When content capture is NO_CONTENT: query omitted; documents record id and score only."""
121121
retriever = FakeRetriever()
122122
retriever.invoke("secret query")
123123

124124
retriever_spans = _find_retriever_spans(span_exporter)
125125
assert len(retriever_spans) >= 1
126126
attrs = dict(retriever_spans[0].attributes)
127127

128-
assert GEN_AI_RETRIEVAL_QUERY not in attrs, (
129-
"Retrieval query should NOT be captured when content capture is disabled"
128+
assert GEN_AI_RETRIEVAL_QUERY_TEXT not in attrs, (
129+
"Query should NOT be captured when content capture is disabled"
130130
)
131-
assert GEN_AI_RETRIEVAL_DOCUMENTS not in attrs, (
132-
"Retrieval documents should NOT be captured when content capture is disabled"
131+
# Documents are recorded with id and score only (no content) when NO_CONTENT
132+
docs_val = attrs.get(GEN_AI_RETRIEVAL_DOCUMENTS, "")
133+
assert "secret query" not in docs_val, (
134+
"Document content should NOT be captured when NO_CONTENT"
133135
)

util/opentelemetry-util-genai/CHANGELOG-loongsuite.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
99

1010
### Added
1111

12+
- Add `RetrievalDocument` dataclass for typed retrieval document representation (id, score, content, metadata).
13+
([#145](https://github.com/alibaba/loongsuite-python-agent/pull/145))
14+
- Control RetrievalDocument serialization: when content capturing is NO_CONTENT, only serialize id and score; when SPAN_ONLY/SPAN_AND_EVENT, serialize full (id, score, content, metadata)
15+
([#145](https://github.com/alibaba/loongsuite-python-agent/pull/145))
1216
- Add Entry span (`gen_ai.span.kind=ENTRY`) and ReAct Step span (`gen_ai.span.kind=STEP`) support in `ExtendedTelemetryHandler` with types, utilities, and context-manager APIs
1317
([#135](https://github.com/alibaba/loongsuite-python-agent/pull/135))
1418
- Propagate `gen_ai.session.id` and `gen_ai.user.id` into Baggage during `start_entry`, enabling traffic coloring via `BaggageSpanProcessor` for all child spans within the entry block
1519
([#135](https://github.com/alibaba/loongsuite-python-agent/pull/135))
1620

1721
### Changed
1822

23+
- **Retrieval semantic convention**: Align retrieval spans with LoongSuite spec
24+
([#145](https://github.com/alibaba/loongsuite-python-agent/pull/145))
25+
- `gen_ai.operation.name`: `retrieve_documents``retrieval`
26+
- `gen_ai.retrieval.query``gen_ai.retrieval.query.text` for query text
27+
- Span name: `retrieval {gen_ai.data_source.id}` when `data_source_id` is set
28+
- Add `RetrievalInvocation` fields: `data_source_id`, `provider`, `request_model`, `top_k`
1929
- Add optional `context` parameter to all `start_*` methods in `TelemetryHandler` and `ExtendedTelemetryHandler` for explicit parent-child span linking
2030
([#135](https://github.com/alibaba/loongsuite-python-agent/pull/135))
2131
- Unify `attach`/`detach` strategy in `ExtendedTelemetryHandler`: always `attach` regardless of whether `context` is provided; `stop_*`/`fail_*` guards restored to `context_token is None or span is None`

util/opentelemetry-util-genai/README-loongsuite.rst

Lines changed: 32 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ LoongSuite 扩展为 OpenTelemetry GenAI Util 包提供了额外的 Generative A
1313
- **create_agent**: Agent 创建操作
1414
- **embedding**: 向量嵌入生成操作
1515
- **execute_tool**: 工具执行操作
16-
- **retrieve**: 文档检索操作(向量数据库查询)
16+
- **retrieval**: 文档检索操作(向量数据库查询)
1717
- **rerank**: 文档重排序操作
1818
- **memory**: 记忆操作,支持记忆的增删改查等操作
1919
- **entry**: AI 应用系统入口标识,支持 session_id/user_id 的 Baggage 传播
@@ -352,36 +352,46 @@ Token 使用:
352352
invocation.tool_call_result = result
353353

354354

355-
6. 文档检索 (retrieve)
355+
6. 文档检索 (retrieval)
356356
~~~~~~~~~~~~~~~~~~~~~~~
357357

358358
用于跟踪从向量数据库或搜索系统检索文档的操作。
359359

360360
**支持的属性:**
361361

362-
- ``gen_ai.operation.name``: 操作名称,固定为 "retrieve"
363-
- ``gen_ai.provider.name``: 提供商名称
364-
- ``gen_ai.retrieval.query``: 检索查询字符串(受内容捕获模式控制)
365-
- ``gen_ai.retrieval.documents``: 检索到的文档(受内容捕获模式控制)
362+
- ``gen_ai.operation.name``: 操作名称,固定为 "retrieval"
363+
- ``gen_ai.span.kind``: 固定为 "RETRIEVER"
364+
- ``gen_ai.data_source.id``: 数据源唯一标识(有条件时必须)
365+
- ``gen_ai.provider.name``: 提供商名称(有条件时必须)
366+
- ``gen_ai.request.model``: 请求模型(有条件时必须)
367+
- ``gen_ai.request.top_k``: 请求 topK(推荐)
368+
- ``gen_ai.retrieval.query.text``: 检索内容短句(可选,受内容捕获模式控制)
369+
- ``gen_ai.retrieval.documents``: 召回的文档列表,格式 [{"id": str, "score": float}, ...](可选,受内容捕获模式控制)
370+
371+
**Span 命名:** ``retrieval {gen_ai.data_source.id}``,无 data_source_id 时为 ``retrieval``
372+
373+
**文档格式:** 使用 ``List[RetrievalDocument]``,instrumentation 需将框架类型(如 LangChain Document)转换为 ``RetrievalDocument``。当 OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT 为 NO_CONTENT 时仅记录 id 和 score;SPAN_ONLY/SPAN_AND_EVENT 时记录完整。
366374

367375
**使用示例:**
368376

369377
::
370378

371-
from opentelemetry.util.genai.extended_types import RetrieveInvocation
379+
from opentelemetry.util.genai.extended_types import RetrievalInvocation, RetrievalDocument
372380

373-
with handler.retrieve() as invocation:
381+
with handler.retrieval() as invocation:
374382
invocation.provider = "chroma"
375-
invocation.retrieval_query = "什么是 OpenTelemetry?"
383+
invocation.data_source_id = "H7STPQYOND"
384+
invocation.query = "什么是 OpenTelemetry?"
385+
invocation.top_k = 5.0
376386
377387
# 执行检索...
378-
invocation.retrieval_documents = [
379-
{"id": "doc1", "content": "OpenTelemetry 是一个观测性框架...", "score": 0.95},
380-
{"id": "doc2", "content": "OpenTelemetry 提供统一的 API...", "score": 0.88}
388+
invocation.documents = [
389+
RetrievalDocument(id="doc1", score=0.95, content="...", metadata={}),
390+
RetrievalDocument(id="doc2", score=0.88, content="...", metadata={}),
381391
]
382392

383393

384-
7. 文档重排序 (rerank)
394+
1. 文档重排序 (rerank)
385395
~~~~~~~~~~~~~~~~~~~~~~~
386396

387397
用于跟踪文档重排序操作,支持基于模型和基于 LLM 的重排序器。
@@ -797,13 +807,18 @@ Baggage 中已有同名 key,则会被覆盖。
797807
tool_inv.tool_call_result = {"products": [...]}
798808

799809
# 检索相关文档
800-
with handler.retrieve() as retrieve_inv:
801-
retrieve_inv.provider = "chroma"
802-
retrieve_inv.retrieval_query = "笔记本电脑推荐"
810+
from opentelemetry.util.genai.extended_types import RetrievalDocument
811+
with handler.retrieval() as retrieval_inv:
812+
retrieval_inv.provider = "chroma"
813+
retrieval_inv.data_source_id = "my_vector_store"
814+
retrieval_inv.query = "笔记本电脑推荐"
803815
804816
# 执行检索...
805817
806-
retrieve_inv.retrieval_documents = [...]
818+
retrieval_inv.documents = [
819+
RetrievalDocument(id="doc1", score=0.95, content="...", metadata={}),
820+
RetrievalDocument(id="doc2", score=0.88, content="...", metadata={}),
821+
]
807822

808823
# 重排序结果
809824
with handler.rerank() as rerank_inv:

util/opentelemetry-util-genai/src/opentelemetry/util/genai/_extended_semconv/gen_ai_extended_attributes.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -43,10 +43,10 @@
4343
The result returned by the tool after execution.
4444
"""
4545

46-
# Retrieve attributes
47-
GEN_AI_RETRIEVAL_QUERY: Final = "gen_ai.retrieval.query"
46+
# Retrieval attributes
47+
GEN_AI_RETRIEVAL_QUERY_TEXT: Final = "gen_ai.retrieval.query.text"
4848
"""
49-
The query string used to retrieve documents from a vector database or search system.
49+
The retrieval query text (short phrase). Per LoongSuite semantic convention.
5050
"""
5151

5252
GEN_AI_RETRIEVAL_DOCUMENTS: Final = "gen_ai.retrieval.documents"
@@ -191,8 +191,8 @@ class GenAiSpanKindValues(Enum):
191191

192192

193193
class GenAiExtendedOperationNameValues(Enum):
194-
RETRIEVE_DOCUMENTS = "retrieve_documents"
195-
"""Retrieve documents operation."""
194+
RETRIEVAL = "retrieval"
195+
"""Retrieval operation (vector store / database lookup). Per LoongSuite semantic convention."""
196196

197197
RERANK_DOCUMENTS = "rerank_documents"
198198
"""Rerank documents operation."""

0 commit comments

Comments
 (0)