-
Notifications
You must be signed in to change notification settings - Fork 3
Fix/HYBIM-961 retrieval and agent span semantics #210
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
541de2d
8e02dc1
94faa32
557f420
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -20,16 +20,12 @@ | |||||||||||||||||||||||||||||||||||||||||||||||
| _NAME_PARTS: dict[StepType, tuple[str, str]] = { | ||||||||||||||||||||||||||||||||||||||||||||||||
| StepType.llm: ("chat", "model"), | ||||||||||||||||||||||||||||||||||||||||||||||||
| StepType.tool: ("execute_tool", "name"), | ||||||||||||||||||||||||||||||||||||||||||||||||
| StepType.retriever: ("retrieval", "name"), | ||||||||||||||||||||||||||||||||||||||||||||||||
| StepType.retriever: ("retrieval", "data_source_id"), | ||||||||||||||||||||||||||||||||||||||||||||||||
| StepType.workflow: ("invoke_workflow", "name"), | ||||||||||||||||||||||||||||||||||||||||||||||||
| StepType.agent: ("invoke_agent", "name"), | ||||||||||||||||||||||||||||||||||||||||||||||||
| StepType.control: ("", "name"), | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| _KIND_BY_STEP_TYPE = { | ||||||||||||||||||||||||||||||||||||||||||||||||
| step_type: SpanKind.CLIENT if step_type is StepType.llm else SpanKind.INTERNAL for step_type in _NAME_PARTS | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| def _step_type(span: BaseStep) -> StepType: | ||||||||||||||||||||||||||||||||||||||||||||||||
| raw_type = getattr(span, "type", None) | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -48,9 +44,21 @@ def _step_type(span: BaseStep) -> StepType: | |||||||||||||||||||||||||||||||||||||||||||||||
| def _span_name(span: BaseStep, step_type: StepType) -> str: | ||||||||||||||||||||||||||||||||||||||||||||||||
| prefix, field_name = _NAME_PARTS[step_type] | ||||||||||||||||||||||||||||||||||||||||||||||||
| detail = getattr(span, field_name, None) | ||||||||||||||||||||||||||||||||||||||||||||||||
| if step_type is StepType.retriever: | ||||||||||||||||||||||||||||||||||||||||||||||||
| if detail is not None: | ||||||||||||||||||||||||||||||||||||||||||||||||
| return f"{prefix} {detail}" | ||||||||||||||||||||||||||||||||||||||||||||||||
| detail = getattr(span, "name", None) | ||||||||||||||||||||||||||||||||||||||||||||||||
| return " ".join(part for part in (prefix, str(detail).strip() if detail is not None else "") if part) | ||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+47
to
51
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 minor (bug): The retriever branch returns
That is the exact class of malformed name
@field_validator("data_source_id", mode="before")
@classmethod
def normalize_data_source_id(cls, value: object) -> object:
if isinstance(value, str):
stripped = value.strip()
return stripped or None
return valueNote option 2 changes the intent of
Suggested change
🤖 Generated by the Astra agent
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Agreed whitespace only IDs are now absent. Padded IDs are trimmed consistently before use in both the span name and attribute.
Comment on lines
46
to
51
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 minor (bug): The retriever branch reads Not reachable through
Suggested change
🤖 Generated by the Astra agent |
||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| def _span_kind(span: BaseStep, step_type: StepType) -> SpanKind: | ||||||||||||||||||||||||||||||||||||||||||||||||
| if step_type in (StepType.llm, StepType.retriever): | ||||||||||||||||||||||||||||||||||||||||||||||||
| return SpanKind.CLIENT | ||||||||||||||||||||||||||||||||||||||||||||||||
| if step_type is StepType.agent and getattr(span, "span_kind", None) is SpanKind.CLIENT: | ||||||||||||||||||||||||||||||||||||||||||||||||
| return SpanKind.CLIENT | ||||||||||||||||||||||||||||||||||||||||||||||||
| return SpanKind.INTERNAL | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| def _to_unix_ns(value: datetime) -> int: | ||||||||||||||||||||||||||||||||||||||||||||||||
| if value.tzinfo is None: | ||||||||||||||||||||||||||||||||||||||||||||||||
| value = value.replace(tzinfo=UTC) | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -98,7 +106,7 @@ def convert_span( | |||||||||||||||||||||||||||||||||||||||||||||||
| attributes=build_span_attributes(span, session_id), | ||||||||||||||||||||||||||||||||||||||||||||||||
| events=(), | ||||||||||||||||||||||||||||||||||||||||||||||||
| links=(), | ||||||||||||||||||||||||||||||||||||||||||||||||
| kind=_KIND_BY_STEP_TYPE[step_type], | ||||||||||||||||||||||||||||||||||||||||||||||||
| kind=_span_kind(span, step_type), | ||||||||||||||||||||||||||||||||||||||||||||||||
| instrumentation_scope=_INSTRUMENTATION_SCOPE, | ||||||||||||||||||||||||||||||||||||||||||||||||
| status=_span_status(span), | ||||||||||||||||||||||||||||||||||||||||||||||||
| start_time=start_time_ns, | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -55,6 +55,7 @@ def call_llm(prompt, temperature=0.7): | |
| from types import TracebackType | ||
| from typing import Any, TypeVar, cast, overload | ||
|
|
||
| from opentelemetry.trace import SpanKind | ||
| from typing_extensions import ParamSpec | ||
|
|
||
| from galileo_core.schemas.logging.span import WorkflowSpan | ||
|
|
@@ -276,6 +277,8 @@ def log( | |
| name: str | None = None, | ||
| span_type: SPAN_TYPE | None = None, | ||
| params: dict[str, str | Callable] | None = None, | ||
| data_source_id: str | None = None, | ||
| span_kind: SpanKind | None = None, | ||
| ) -> Callable[[Callable[P, R]], Callable[P, R]]: ... | ||
|
|
||
| def log( | ||
|
|
@@ -286,6 +289,8 @@ def log( | |
| span_type: SPAN_TYPE | None = None, | ||
| params: dict[str, str | Callable] | None = None, | ||
| dataset_record: DatasetRecord | None = None, | ||
| data_source_id: str | None = None, | ||
| span_kind: SpanKind | None = None, | ||
| ) -> Callable[[Callable[P, R]], Callable[P, R]]: | ||
| """ | ||
| Main decorator function for logging function calls. | ||
|
|
@@ -304,33 +309,66 @@ def log( | |
| Optional span type ("llm", "retriever", "tool", "workflow", "agent") | ||
| params | ||
| Optional parameter mapping for extracting specific values | ||
| data_source_id | ||
| Optional authoritative retrieval data-source ID for ``span_type="retriever"``. | ||
| span_kind | ||
| Optional OTel kind for ``span_type="agent"``. Only ``SpanKind.CLIENT`` marks a remote agent call. | ||
| dataset_record | ||
| Optional parameter for dataset values. This is used by the local experiment module to set the dataset fields on the trace/spans and not generally provided for logging to log streams. | ||
|
|
||
| Returns | ||
| ------- | ||
| A decorated function that logs its execution | ||
| """ | ||
| explicit_span_params = { | ||
| key: value | ||
| for key, value in {"data_source_id": data_source_id, "span_kind": span_kind}.items() | ||
| if value is not None | ||
| } | ||
|
|
||
| def decorator(func: Callable[P, R]) -> Callable[P, R]: | ||
| if inspect.isasyncgenfunction(func): | ||
| return cast( | ||
| Callable[P, R], | ||
| self._async_generator_log( | ||
| func, name=name, span_type=span_type, params=params, dataset_record=dataset_record | ||
| func, | ||
| name=name, | ||
| span_type=span_type, | ||
| params=params, | ||
| dataset_record=dataset_record, | ||
| explicit_span_params=explicit_span_params, | ||
| ), | ||
| ) | ||
| if inspect.isgeneratorfunction(func): | ||
| return cast( | ||
| Callable[P, R], | ||
| self._sync_generator_log( | ||
| func, name=name, span_type=span_type, params=params, dataset_record=dataset_record | ||
| func, | ||
| name=name, | ||
| span_type=span_type, | ||
| params=params, | ||
| dataset_record=dataset_record, | ||
| explicit_span_params=explicit_span_params, | ||
| ), | ||
| ) | ||
| wrapped = ( | ||
| self._async_log(func, name=name, span_type=span_type, params=params, dataset_record=dataset_record) | ||
| self._async_log( | ||
| func, | ||
| name=name, | ||
| span_type=span_type, | ||
| params=params, | ||
| dataset_record=dataset_record, | ||
| explicit_span_params=explicit_span_params, | ||
| ) | ||
| if asyncio.iscoroutinefunction(func) | ||
| else self._sync_log(func, name=name, span_type=span_type, params=params, dataset_record=dataset_record) | ||
| else self._sync_log( | ||
| func, | ||
| name=name, | ||
| span_type=span_type, | ||
| params=params, | ||
| dataset_record=dataset_record, | ||
| explicit_span_params=explicit_span_params, | ||
| ) | ||
| ) | ||
| return cast(Callable[P, R], wrapped) | ||
|
|
||
|
|
@@ -348,6 +386,7 @@ def _async_log( | |
| span_type: SPAN_TYPE | None, | ||
| params: dict[str, str | Callable] | None = None, | ||
| dataset_record: DatasetRecord | None = None, | ||
| explicit_span_params: dict[str, Any] | None = None, | ||
| ) -> F: | ||
| """ | ||
| Internal method to handle logging for async functions. | ||
|
|
@@ -384,6 +423,7 @@ async def async_wrapper(*args: Any, **kwargs: Any) -> Any: | |
| name=name or func.__name__, | ||
| span_type=span_type, | ||
| params=params, | ||
| explicit_span_params=explicit_span_params, | ||
| is_method=self._is_method(func), | ||
| func_args=args, | ||
| func_kwargs=kwargs, | ||
|
|
@@ -415,6 +455,7 @@ def _sync_log( | |
| span_type: SPAN_TYPE | None, | ||
| params: dict[str, str | Callable] | None = None, | ||
| dataset_record: DatasetRecord | None = None, | ||
| explicit_span_params: dict[str, Any] | None = None, | ||
| ) -> F: | ||
| """ | ||
| Internal method to handle logging for synchronous functions. | ||
|
|
@@ -442,6 +483,7 @@ def sync_wrapper(*args: Any, **kwargs: Any) -> Any: | |
| name=name or func.__name__, | ||
| span_type=span_type, | ||
| params=params, | ||
| explicit_span_params=explicit_span_params, | ||
| is_method=self._is_method(func), | ||
| func_args=args, | ||
| func_kwargs=kwargs, | ||
|
|
@@ -473,6 +515,7 @@ def _sync_generator_log( | |
| span_type: SPAN_TYPE | None, | ||
| params: dict[str, str | Callable] | None = None, | ||
| dataset_record: DatasetRecord | None = None, | ||
| explicit_span_params: dict[str, Any] | None = None, | ||
| ) -> F: | ||
| @wraps(func) | ||
| def generator_wrapper(*args: Any, **kwargs: Any) -> Generator: | ||
|
|
@@ -481,6 +524,7 @@ def generator_wrapper(*args: Any, **kwargs: Any) -> Generator: | |
| name=name or func.__name__, | ||
| span_type=span_type, | ||
| params=params, | ||
| explicit_span_params=explicit_span_params, | ||
| is_method=self._is_method(func), | ||
| func_args=args, | ||
| func_kwargs=kwargs, | ||
|
|
@@ -500,6 +544,7 @@ def _async_generator_log( | |
| span_type: SPAN_TYPE | None, | ||
| params: dict[str, str | Callable] | None = None, | ||
| dataset_record: DatasetRecord | None = None, | ||
| explicit_span_params: dict[str, Any] | None = None, | ||
| ) -> F: | ||
| @wraps(func) | ||
| def async_generator_wrapper(*args: Any, **kwargs: Any) -> AsyncGenerator: | ||
|
|
@@ -508,6 +553,7 @@ def async_generator_wrapper(*args: Any, **kwargs: Any) -> AsyncGenerator: | |
| name=name or func.__name__, | ||
| span_type=span_type, | ||
| params=params, | ||
| explicit_span_params=explicit_span_params, | ||
| is_method=self._is_method(func), | ||
| func_args=args, | ||
| func_kwargs=kwargs, | ||
|
|
@@ -543,6 +589,7 @@ def _prepare_input( | |
| name: str, | ||
| span_type: SPAN_TYPE | None, | ||
| params: dict[str, str | Callable] | None = None, | ||
| explicit_span_params: dict[str, Any] | None = None, | ||
| is_method: bool = False, | ||
| func_args: tuple = (), | ||
| func_kwargs: dict | None = None, | ||
|
|
@@ -606,6 +653,8 @@ def _prepare_input( | |
| if param_name in input_ and param_name not in span_params: | ||
| span_params[param_name] = input_[param_name] | ||
|
|
||
| span_params.update(explicit_span_params or {}) | ||
|
|
||
| if "name" not in span_params: | ||
| span_params["name"] = name | ||
|
|
||
|
|
@@ -685,10 +734,10 @@ def _get_span_param_names(self, span_type: SPAN_TYPE) -> list[str]: | |
| common_params = ["name", "input", "metadata", "tags"] | ||
| span_params = { | ||
| "llm": [*common_params, "model", "temperature", "tools"], | ||
| "retriever": common_params, | ||
| "retriever": [*common_params, "data_source_id"], | ||
| "tool": [*common_params, "tool_call_id"], | ||
| "workflow": common_params, | ||
| "agent": [*common_params, "agent_type"], | ||
| "agent": [*common_params, "agent_type", "span_kind"], | ||
|
Comment on lines
+737
to
+740
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 minor (bug): Adding Two consequences worth weighing:
This mirrors how 🤖 Generated by the Astra agent |
||
| } | ||
| return span_params.get(span_type, common_params) | ||
|
|
||
|
|
@@ -791,8 +840,9 @@ def _prepare_call( | |
| created_at = span_params.get("created_at", _get_timestamp()) | ||
| if span_type == "agent": | ||
| agent_type = span_params.get("agent_type") | ||
| span_kind = span_params.get("span_kind", SpanKind.INTERNAL) | ||
| span = client_instance.add_agent_span( | ||
| input=input_, name=name, agent_type=agent_type, created_at=created_at | ||
| input=input_, name=name, agent_type=agent_type, span_kind=span_kind, created_at=created_at | ||
| ) | ||
| else: | ||
| span = client_instance.add_workflow_span(input=input_, name=name, created_at=created_at) | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🟡 minor (bug):
gen_ai.data_source.idis emitted but never registered inSPLUNK_ALIAS_BY_GEN_AI, sonormalize_attributes_for_exportwill not produce asplunk_ao.*mirror for it. Every othergen_ai.*attribute this function emits has an alias — including the two immediate neighbours here,gen_ai.retrieval.query.textandgen_ai.retrieval.top_k. The result is that with normalization enabled (SPLUNK_AO_DEV_ENABLE_ATTRIBUTE_NORMALIZATION), the new authoritative retrieval identity is the only retrieval field with no Splunk-namespaced counterpart, so any backend or dashboard consuming thesplunk_ao.retrieval.*set silently sees no data source ID.Impact is limited today because normalization is opt-in and off by default, which is why I'm rating this minor rather than major — but it will become a silent data gap the moment normalization is turned on. Add the alias next to the other retrieval entries.
test_every_alias_uses_an_explicit_destination_namespaceonly checks namespacing, not coverage, so nothing currently catches this; consider a companion assertion that everygen_ai.*key produced bybuild_span_attributesappears in the alias map.🤖 Generated by the Astra agent
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
gen_ai.data_source.idmust remain standard only, the normalization behavior will be removed fully in future