1- from typing import Any , Dict , List , Optional , Tuple
1+ import base64
2+ import json
3+ from typing import Any , Callable , Dict , List , Optional , Tuple
24
35import chevron
46from ldclient import Context
@@ -61,14 +63,53 @@ def __init__(self, client: LDClient):
6163 1 ,
6264 )
6365
66+ def create_tracker (self , token : str , context : Context ) -> LDAIConfigTracker :
67+ """
68+ Reconstruct a tracker from a resumption token.
69+
70+ This is used for cross-process scenarios such as deferred feedback,
71+ where a different service needs to associate tracking events with the
72+ original execution's ``runId``.
73+
74+ :param token: A URL-safe Base64-encoded resumption token obtained from
75+ :attr:`LDAIConfigTracker.resumption_token`.
76+ :param context: The context to use for track events.
77+ :return: A new :class:`LDAIConfigTracker` bound to the original
78+ ``runId`` from the token.
79+ :raises ValueError: If the token is invalid or missing required fields.
80+ """
81+ try :
82+ # Add padding back before decoding
83+ padded = token + "=" * (- len (token ) % 4 )
84+ payload = json .loads (
85+ base64 .urlsafe_b64decode (padded .encode ("utf-8" )).decode ("utf-8" )
86+ )
87+ except (json .JSONDecodeError , Exception ) as e :
88+ raise ValueError (f"Invalid resumption token: { e } " ) from e
89+
90+ for field in ("runId" , "configKey" , "version" ):
91+ if field not in payload :
92+ raise ValueError (f"Invalid resumption token: missing required field '{ field } '" )
93+
94+ return LDAIConfigTracker (
95+ ld_client = self ._client ,
96+ variation_key = payload .get ("variationKey" , "" ),
97+ config_key = payload ["configKey" ],
98+ version = payload ["version" ],
99+ model_name = "" ,
100+ provider_name = "" ,
101+ context = context ,
102+ run_id = payload ["runId" ],
103+ )
104+
64105 def _completion_config (
65106 self ,
66107 key : str ,
67108 context : Context ,
68109 default : AICompletionConfigDefault ,
69110 variables : Optional [Dict [str , Any ]] = None ,
70111 ) -> AICompletionConfig :
71- model , provider , messages , instructions , tracker , enabled , judge_configuration , _ = self .__evaluate (
112+ model , provider , messages , instructions , tracker , tracker_factory , enabled , judge_configuration , _ = self .__evaluate (
72113 key , context , default .to_dict (), variables
73114 )
74115
@@ -79,6 +120,7 @@ def _completion_config(
79120 messages = messages ,
80121 provider = provider ,
81122 tracker = tracker ,
123+ create_tracker = tracker_factory if enabled else None ,
82124 judge_configuration = judge_configuration ,
83125 )
84126
@@ -134,7 +176,7 @@ def _judge_config(
134176 default : AIJudgeConfigDefault ,
135177 variables : Optional [Dict [str , Any ]] = None ,
136178 ) -> AIJudgeConfig :
137- model , provider , messages , instructions , tracker , enabled , judge_configuration , variation = self .__evaluate (
179+ model , provider , messages , instructions , tracker , tracker_factory , enabled , judge_configuration , variation = self .__evaluate (
138180 key , context , default .to_dict (), variables
139181 )
140182
@@ -163,6 +205,7 @@ def _extract_evaluation_metric_key(variation: Dict[str, Any]) -> Optional[str]:
163205 messages = messages ,
164206 provider = provider ,
165207 tracker = tracker ,
208+ create_tracker = tracker_factory if enabled else None ,
166209 )
167210
168211 return config
@@ -248,14 +291,14 @@ async def create_judge(
248291 key , context , default or AIJudgeConfigDefault .disabled (), extended_variables
249292 )
250293
251- if not judge_config .enabled or not judge_config .tracker :
294+ if not judge_config .enabled or not judge_config .create_tracker :
252295 return None
253296
254297 provider = RunnerFactory .create_model (judge_config , default_ai_provider )
255298 if not provider :
256299 return None
257300
258- return Judge (judge_config , judge_config .tracker , provider )
301+ return Judge (judge_config , judge_config .create_tracker () , provider )
259302 except Exception as error :
260303 return None
261304
@@ -345,7 +388,7 @@ async def create_model(
345388 log .debug (f"Creating managed model for key: { key } " )
346389 config = self ._completion_config (key , context , default or AICompletionConfigDefault .disabled (), variables )
347390
348- if not config .enabled or not config .tracker :
391+ if not config .enabled or not config .create_tracker :
349392 return None
350393
351394 runner = RunnerFactory .create_model (config , default_ai_provider )
@@ -361,7 +404,7 @@ async def create_model(
361404 default_ai_provider ,
362405 )
363406
364- return ManagedModel (config , config .tracker , runner , judges )
407+ return ManagedModel (config , config .create_tracker () , runner , judges )
365408
366409 async def create_chat (
367410 self ,
@@ -428,14 +471,14 @@ async def create_agent(
428471 log .debug (f"Creating managed agent for key: { key } " )
429472 config = self .__evaluate_agent (key , context , default or AIAgentConfigDefault .disabled (), variables )
430473
431- if not config .enabled or not config .tracker :
474+ if not config .enabled or not config .create_tracker :
432475 return None
433476
434477 runner = RunnerFactory .create_agent (config , tools or {}, default_ai_provider )
435478 if not runner :
436479 return None
437480
438- return ManagedAgent (config , config .tracker , runner )
481+ return ManagedAgent (config , config .create_tracker () , runner )
439482
440483 def agent_config (
441484 self ,
@@ -750,7 +793,7 @@ def __evaluate(
750793 variables : Optional [Dict [str , Any ]] = None ,
751794 ) -> Tuple [
752795 Optional [ModelConfig ], Optional [ProviderConfig ], Optional [List [LDMessage ]],
753- Optional [str ], LDAIConfigTracker , bool , Optional [Any ], Dict [str , Any ]
796+ Optional [str ], LDAIConfigTracker , Callable [[], LDAIConfigTracker ], bool , Optional [Any ], Dict [str , Any ]
754797 ]:
755798 """
756799 Internal method to evaluate a configuration and extract components.
@@ -801,15 +844,23 @@ def __evaluate(
801844 custom = custom
802845 )
803846
804- tracker = LDAIConfigTracker (
805- self ._client ,
806- variation .get ('_ldMeta' , {}).get ('variationKey' , '' ),
807- key ,
808- int (variation .get ('_ldMeta' , {}).get ('version' , 1 )),
809- model .name if model else '' ,
810- provider_config .name if provider_config else '' ,
811- context ,
812- )
847+ variation_key = variation .get ('_ldMeta' , {}).get ('variationKey' , '' )
848+ version = int (variation .get ('_ldMeta' , {}).get ('version' , 1 ))
849+ model_name = model .name if model else ''
850+ provider_name = provider_config .name if provider_config else ''
851+
852+ def tracker_factory () -> LDAIConfigTracker :
853+ return LDAIConfigTracker (
854+ self ._client ,
855+ variation_key ,
856+ key ,
857+ version ,
858+ model_name ,
859+ provider_name ,
860+ context ,
861+ )
862+
863+ tracker = tracker_factory ()
813864
814865 enabled = variation .get ('_ldMeta' , {}).get ('enabled' , False )
815866
@@ -828,7 +879,7 @@ def __evaluate(
828879 if judges :
829880 judge_configuration = JudgeConfiguration (judges = judges )
830881
831- return model , provider_config , messages , instructions , tracker , enabled , judge_configuration , variation
882+ return model , provider_config , messages , instructions , tracker , tracker_factory , enabled , judge_configuration , variation
832883
833884 def __evaluate_agent (
834885 self ,
@@ -846,7 +897,7 @@ def __evaluate_agent(
846897 :param variables: Variables for interpolation.
847898 :return: Configured AIAgentConfig instance.
848899 """
849- model , provider , messages , instructions , tracker , enabled , judge_configuration , _ = self .__evaluate (
900+ model , provider , messages , instructions , tracker , tracker_factory , enabled , judge_configuration , _ = self .__evaluate (
850901 key , context , default .to_dict (), variables
851902 )
852903
@@ -860,6 +911,7 @@ def __evaluate_agent(
860911 provider = provider or default .provider ,
861912 instructions = final_instructions ,
862913 tracker = tracker ,
914+ create_tracker = tracker_factory if enabled else None ,
863915 judge_configuration = judge_configuration or default .judge_configuration ,
864916 )
865917
0 commit comments