-
Notifications
You must be signed in to change notification settings - Fork 246
Extract variable resolution structure #1947
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
Changes from 2 commits
bbb9f94
be4926e
796e333
817ade8
52fec27
81eb0ee
7f0b05d
611b4b1
5b16a87
01570d3
e4d85e6
36539fc
25247ac
019ae7a
69edbc4
3aa3c82
b7181a4
d19c2d7
0b24626
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 | ||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -4,7 +4,7 @@ | |||||||||||
| from collections.abc import Generator, Mapping, Sequence | ||||||||||||
| from contextlib import ExitStack, contextmanager | ||||||||||||
| from contextvars import ContextVar | ||||||||||||
| from dataclasses import dataclass, field, replace | ||||||||||||
| from dataclasses import dataclass, field | ||||||||||||
| from importlib.util import find_spec | ||||||||||||
| from typing import TYPE_CHECKING, Any, Generic, Protocol, TypeVar | ||||||||||||
|
|
||||||||||||
|
|
@@ -27,6 +27,7 @@ | |||||||||||
| __all__ = ( | ||||||||||||
| 'ResolveFunction', | ||||||||||||
| 'is_resolve_function', | ||||||||||||
| '_BaseVariable', | ||||||||||||
| 'Variable', | ||||||||||||
| 'targeting_context', | ||||||||||||
| ) | ||||||||||||
|
|
@@ -37,6 +38,15 @@ | |||||||||||
| _VARIABLE_OVERRIDES: ContextVar[dict[str, Any] | None] = ContextVar('_VARIABLE_OVERRIDES', default=None) | ||||||||||||
|
|
||||||||||||
|
|
||||||||||||
| def _record_exception(exception: BaseException, span: logfire.LogfireSpan) -> None: | ||||||||||||
| """Record an exception on a span, ignoring a CPython traceback extraction bug.""" | ||||||||||||
| try: | ||||||||||||
| span.record_exception(exception) | ||||||||||||
| except RuntimeError as exc: | ||||||||||||
| if 'generator raised StopIteration' not in str(exc): | ||||||||||||
|
cubic-dev-ai[bot] marked this conversation as resolved.
Outdated
|
||||||||||||
| raise | ||||||||||||
|
|
||||||||||||
|
|
||||||||||||
| @dataclass | ||||||||||||
| class _TargetingContextData: | ||||||||||||
| """Internal data structure for targeting context.""" | ||||||||||||
|
|
@@ -115,8 +125,8 @@ def is_resolve_function(f: Any) -> TypeIs[ResolveFunction[Any]]: | |||||||||||
| return required_positional <= 2 and total_positional >= 2 | ||||||||||||
|
|
||||||||||||
|
|
||||||||||||
| class Variable(Generic[T_co]): | ||||||||||||
| """A managed variable that can be resolved dynamically based on configuration.""" | ||||||||||||
| class _BaseVariable(Generic[T_co]): | ||||||||||||
| """Base class for managed variables with shared resolution infrastructure.""" | ||||||||||||
|
|
||||||||||||
| name: str | ||||||||||||
| """Unique name identifying this variable.""" | ||||||||||||
|
|
@@ -187,28 +197,12 @@ def refresh_sync(self, force: bool = False): | |||||||||||
| """Synchronously refresh the variable.""" | ||||||||||||
| self.logfire_instance.config.get_variable_provider().refresh(force=force) | ||||||||||||
|
|
||||||||||||
| def get( | ||||||||||||
| def _get_result_and_record_span( | ||||||||||||
| self, | ||||||||||||
| targeting_key: str | None = None, | ||||||||||||
| attributes: Mapping[str, Any] | None = None, | ||||||||||||
| *, | ||||||||||||
| label: str | None = None, | ||||||||||||
| ) -> ResolvedVariable[T_co]: | ||||||||||||
| """Resolve the variable and return full details including label, version, and any errors. | ||||||||||||
|
|
||||||||||||
| Args: | ||||||||||||
| targeting_key: Optional key for deterministic label selection (e.g., user ID). | ||||||||||||
| If not provided, falls back to contextvar targeting key (set via targeting_context), | ||||||||||||
| then to the current trace ID if there is an active trace. | ||||||||||||
| attributes: Optional attributes for condition-based targeting rules. | ||||||||||||
| label: Optional explicit label name to select. If provided, bypasses rollout | ||||||||||||
| weights and targeting, directly selecting the specified label. If the label | ||||||||||||
| doesn't exist in the configuration, falls back to default resolution. | ||||||||||||
|
|
||||||||||||
| Returns: | ||||||||||||
| A ResolvedVariable object containing the resolved value, selected label, | ||||||||||||
| version, and any errors that occurred. | ||||||||||||
| """ | ||||||||||||
| merged_attributes = self._get_merged_attributes(attributes) | ||||||||||||
|
|
||||||||||||
| # Targeting key resolution: call-site > contextvar > trace_id | ||||||||||||
|
|
@@ -248,13 +242,11 @@ def get( | |||||||||||
| 'value': serialized_value, | ||||||||||||
| 'label': result.label, | ||||||||||||
| 'version': result.version, | ||||||||||||
| 'reason': result._reason, # pyright: ignore[reportPrivateUsage] | ||||||||||||
| 'reason': result.reason, | ||||||||||||
| } | ||||||||||||
| ) | ||||||||||||
| if result.exception: | ||||||||||||
| span.record_exception( | ||||||||||||
| result.exception, | ||||||||||||
| ) | ||||||||||||
| _record_exception(result.exception, span) | ||||||||||||
| return result | ||||||||||||
|
|
||||||||||||
| def _resolve( | ||||||||||||
|
|
@@ -270,7 +262,7 @@ def _resolve( | |||||||||||
| context_value = context_overrides[self.name] | ||||||||||||
| if is_resolve_function(context_value): | ||||||||||||
| context_value = context_value(targeting_key, attributes) | ||||||||||||
| return ResolvedVariable(name=self.name, value=context_value, _reason='context_override') | ||||||||||||
| return ResolvedVariable(name=self.name, value=context_value, reason='context_override') | ||||||||||||
|
|
||||||||||||
| provider = self.logfire_instance.config.get_variable_provider() | ||||||||||||
|
|
||||||||||||
|
|
@@ -286,21 +278,35 @@ def _resolve( | |||||||||||
| span.set_attribute('invalid_serialized_value', serialized_result.value) | ||||||||||||
| default = self._get_default(targeting_key, attributes) | ||||||||||||
| reason: str = 'validation_error' if isinstance(value_or_exc, ValidationError) else 'other_error' | ||||||||||||
| return ResolvedVariable(name=self.name, value=default, exception=value_or_exc, _reason=reason) | ||||||||||||
| return ResolvedVariable( | ||||||||||||
| name=self.name, | ||||||||||||
| value=default, | ||||||||||||
| exception=value_or_exc, | ||||||||||||
| reason=reason, | ||||||||||||
| label=serialized_result.label, | ||||||||||||
| version=serialized_result.version, | ||||||||||||
| ) | ||||||||||||
| return ResolvedVariable( | ||||||||||||
| name=self.name, | ||||||||||||
| value=value_or_exc, | ||||||||||||
| label=serialized_result.label, | ||||||||||||
| version=serialized_result.version, | ||||||||||||
| _reason='resolved', | ||||||||||||
| reason='resolved', | ||||||||||||
| ) | ||||||||||||
| # Label not found - fall through to default resolution | ||||||||||||
|
|
||||||||||||
| serialized_result = provider.get_serialized_value(self.name, targeting_key, attributes) | ||||||||||||
|
|
||||||||||||
| if serialized_result.value is None: | ||||||||||||
| default = self._get_default(targeting_key, attributes) | ||||||||||||
| return _with_value(serialized_result, default) | ||||||||||||
| # Provider had no value; surface that the code default was used. | ||||||||||||
|
Comment on lines
+246
to
247
Contributor
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. P2: Provider exception from Prompt for AI agents
Suggested change
✅ Addressed in
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. (AI) Addressed in stacked PR #1949. |
||||||||||||
| return ResolvedVariable( | ||||||||||||
| name=self.name, | ||||||||||||
| value=self._get_default(targeting_key, attributes), | ||||||||||||
| exception=serialized_result.exception, | ||||||||||||
| label=serialized_result.label, | ||||||||||||
| version=serialized_result.version, | ||||||||||||
| reason='code_default', | ||||||||||||
| ) | ||||||||||||
|
|
||||||||||||
| # Deserialize - returns T | Exception | ||||||||||||
| value_or_exc = self._deserialize(serialized_result.value) | ||||||||||||
|
|
@@ -310,22 +316,29 @@ def _resolve( | |||||||||||
| span.set_attribute('invalid_serialized_value', serialized_result.value) | ||||||||||||
| default = self._get_default(targeting_key, attributes) | ||||||||||||
| reason: str = 'validation_error' if isinstance(value_or_exc, ValidationError) else 'other_error' | ||||||||||||
| return ResolvedVariable(name=self.name, value=default, exception=value_or_exc, _reason=reason) | ||||||||||||
| return ResolvedVariable( | ||||||||||||
| name=self.name, | ||||||||||||
| value=default, | ||||||||||||
| exception=value_or_exc, | ||||||||||||
| reason=reason, | ||||||||||||
| label=serialized_result.label, | ||||||||||||
| version=serialized_result.version, | ||||||||||||
| ) | ||||||||||||
|
|
||||||||||||
| return ResolvedVariable( | ||||||||||||
| name=self.name, | ||||||||||||
| value=value_or_exc, | ||||||||||||
| label=serialized_result.label, | ||||||||||||
| version=serialized_result.version, | ||||||||||||
| _reason='resolved', | ||||||||||||
| reason='resolved', | ||||||||||||
| ) | ||||||||||||
|
|
||||||||||||
| except Exception as e: | ||||||||||||
| if span and serialized_result is not None: # pragma: no cover | ||||||||||||
| span.set_attribute('invalid_serialized_label', serialized_result.label) | ||||||||||||
| span.set_attribute('invalid_serialized_value', serialized_result.value) | ||||||||||||
| default = self._get_default(targeting_key, attributes) | ||||||||||||
| return ResolvedVariable(name=self.name, value=default, exception=e, _reason='other_error') | ||||||||||||
| return ResolvedVariable(name=self.name, value=default, exception=e, reason='other_error') | ||||||||||||
|
|
||||||||||||
| def _get_default( | ||||||||||||
| self, targeting_key: str | None = None, merged_attributes: Mapping[str, Any] | None = None | ||||||||||||
|
|
@@ -385,17 +398,32 @@ def to_config(self) -> VariableConfig: | |||||||||||
| ) | ||||||||||||
|
|
||||||||||||
|
|
||||||||||||
| def _with_value(details: ResolvedVariable[Any], new_value: T_co) -> ResolvedVariable[T_co]: | ||||||||||||
| """Return a copy of the provided resolution details, just with a different value. | ||||||||||||
| class Variable(_BaseVariable[T_co]): | ||||||||||||
| """A managed variable that can be resolved dynamically based on configuration.""" | ||||||||||||
|
|
||||||||||||
| Args: | ||||||||||||
| details: Existing resolution details to modify. | ||||||||||||
| new_value: The new value to use. | ||||||||||||
| def get( | ||||||||||||
| self, | ||||||||||||
| targeting_key: str | None = None, | ||||||||||||
| attributes: Mapping[str, Any] | None = None, | ||||||||||||
| *, | ||||||||||||
| label: str | None = None, | ||||||||||||
| ) -> ResolvedVariable[T_co]: | ||||||||||||
| """Resolve the variable and return full details including label, version, and any errors. | ||||||||||||
|
|
||||||||||||
| Returns: | ||||||||||||
| A new ResolvedVariable with the given value. | ||||||||||||
| """ | ||||||||||||
| return replace(details, value=new_value) | ||||||||||||
| Args: | ||||||||||||
| targeting_key: Optional key for deterministic label selection (e.g., user ID). | ||||||||||||
| If not provided, falls back to contextvar targeting key (set via targeting_context), | ||||||||||||
| then to the current trace ID if there is an active trace. | ||||||||||||
| attributes: Optional attributes for condition-based targeting rules. | ||||||||||||
| label: Optional explicit label name to select. If provided, bypasses rollout | ||||||||||||
| weights and targeting, directly selecting the specified label. If the label | ||||||||||||
| doesn't exist in the configuration, falls back to default resolution. | ||||||||||||
|
|
||||||||||||
| Returns: | ||||||||||||
| A ResolvedVariable object containing the resolved value, selected label, | ||||||||||||
| version, and any errors that occurred. | ||||||||||||
| """ | ||||||||||||
| return self._get_result_and_record_span(targeting_key, attributes, label) | ||||||||||||
|
|
||||||||||||
|
|
||||||||||||
| @contextmanager | ||||||||||||
|
|
||||||||||||
Uh oh!
There was an error while loading. Please reload this page.