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 ,
@@ -754,7 +797,7 @@ def __evaluate(
754797 graph_key : Optional [str ] = None ,
755798 ) -> Tuple [
756799 Optional [ModelConfig ], Optional [ProviderConfig ], Optional [List [LDMessage ]],
757- Optional [str ], LDAIConfigTracker , bool , Optional [Any ], Dict [str , Any ]
800+ Optional [str ], LDAIConfigTracker , Callable [[], LDAIConfigTracker ], bool , Optional [Any ], Dict [str , Any ]
758801 ]:
759802 """
760803 Internal method to evaluate a configuration and extract components.
@@ -806,16 +849,24 @@ def __evaluate(
806849 custom = custom
807850 )
808851
809- tracker = LDAIConfigTracker (
810- self ._client ,
811- variation .get ('_ldMeta' , {}).get ('variationKey' , '' ),
812- key ,
813- int (variation .get ('_ldMeta' , {}).get ('version' , 1 )),
814- model .name if model else '' ,
815- provider_config .name if provider_config else '' ,
816- context ,
817- graph_key = graph_key ,
818- )
852+ variation_key = variation .get ('_ldMeta' , {}).get ('variationKey' , '' )
853+ version = int (variation .get ('_ldMeta' , {}).get ('version' , 1 ))
854+ model_name = model .name if model else ''
855+ provider_name = provider_config .name if provider_config else ''
856+
857+ def tracker_factory () -> LDAIConfigTracker :
858+ return LDAIConfigTracker (
859+ self ._client ,
860+ variation_key ,
861+ key ,
862+ version ,
863+ model_name ,
864+ provider_name ,
865+ context ,
866+ graph_key = graph_key ,
867+ )
868+
869+ tracker = tracker_factory ()
819870
820871 enabled = variation .get ('_ldMeta' , {}).get ('enabled' , False )
821872
@@ -834,7 +885,7 @@ def __evaluate(
834885 if judges :
835886 judge_configuration = JudgeConfiguration (judges = judges )
836887
837- return model , provider_config , messages , instructions , tracker , enabled , judge_configuration , variation
888+ return model , provider_config , messages , instructions , tracker , tracker_factory , enabled , judge_configuration , variation
838889
839890 def __evaluate_agent (
840891 self ,
@@ -854,7 +905,7 @@ def __evaluate_agent(
854905 :param graph_key: When set, passed to the tracker so all events include ``graphKey``.
855906 :return: Configured AIAgentConfig instance.
856907 """
857- model , provider , messages , instructions , tracker , enabled , judge_configuration , _ = self .__evaluate (
908+ model , provider , messages , instructions , tracker , tracker_factory , enabled , judge_configuration , _ = self .__evaluate (
858909 key , context , default .to_dict (), variables , graph_key = graph_key
859910 )
860911
@@ -868,6 +919,7 @@ def __evaluate_agent(
868919 provider = provider or default .provider ,
869920 instructions = final_instructions ,
870921 tracker = tracker ,
922+ create_tracker = tracker_factory if enabled else None ,
871923 judge_configuration = judge_configuration or default .judge_configuration ,
872924 )
873925
0 commit comments