4949 LDApiClient ,
5050)
5151from ldai_optimizer .prompts import (
52- _acceptance_criteria_implies_cost_optimization ,
53- _acceptance_criteria_implies_duration_optimization ,
5452 build_message_history_text ,
5553 build_new_variation_prompt ,
5654 build_reasoning_history ,
@@ -850,9 +848,7 @@ async def _evaluate_acceptance_judge(
850848
851849 if (
852850 agent_duration_ms is not None
853- and _acceptance_criteria_implies_duration_optimization (
854- {judge_key : optimization_judge }
855- )
851+ and bool (self ._options .latency_optimization )
856852 ):
857853 baseline_ms = self ._baseline_duration_ms
858854 instructions += (
@@ -875,7 +871,7 @@ async def _evaluate_acceptance_judge(
875871 "These suggestions will be used directly to generate the next variation."
876872 )
877873
878- if _acceptance_criteria_implies_cost_optimization ({ judge_key : optimization_judge } ):
874+ if bool ( self . _options . token_optimization ):
879875 current_cost = estimate_cost (
880876 agent_usage ,
881877 _find_model_config (self ._current_model or "" , self ._model_configs ),
@@ -975,7 +971,12 @@ async def _evaluate_acceptance_judge(
975971 return dataclasses .replace (judge_result , duration_ms = judge_duration_ms , usage = judge_response .usage )
976972
977973 async def _get_agent_config (
978- self , agent_key : str , context : Context
974+ self ,
975+ agent_key : str ,
976+ context : Context ,
977+ variation_key : Optional [str ] = None ,
978+ project_key : Optional [str ] = None ,
979+ base_url : Optional [str ] = None ,
979980 ) -> AIAgentConfig :
980981 """
981982 Fetch the agent configuration, replacing the instructions with the raw variation
@@ -985,16 +986,39 @@ async def _get_agent_config(
985986 (including the tracker). We then call variation() separately to retrieve the
986987 unrendered instruction template and swap it in, keeping everything else intact.
987988
989+ When ``variation_key`` is provided the specific variation is fetched via the
990+ LaunchDarkly REST API instead of using the SDK's default flag evaluation.
991+
988992 :param agent_key: The key for the agent to get the configuration for
989993 :param context: The evaluation context
994+ :param variation_key: Optional specific variation key to use as the base
995+ :param project_key: LaunchDarkly project key; required when variation_key is set
996+ :param base_url: Optional API base URL override
990997 :return: AIAgentConfig with raw {{placeholder}} instruction templates intact
991998 """
992999 try :
9931000 agent_config = self ._ldClient .agent_config (agent_key , context )
9941001
995- # variation() returns the raw JSON before chevron.render(), so instructions
996- # still contain {{placeholder}} tokens rather than empty strings.
997- raw_variation = self ._ldClient ._client .variation (agent_key , context , {})
1002+ if variation_key :
1003+ assert self ._api_key is not None
1004+ api_client = LDApiClient (
1005+ self ._api_key ,
1006+ ** ({"base_url" : base_url } if base_url else {}),
1007+ )
1008+ ai_config = api_client .get_ai_config (project_key , agent_key )
1009+ match = next (
1010+ (v for v in (ai_config or {}).get ("variations" , []) if v .get ("key" ) == variation_key ),
1011+ None ,
1012+ )
1013+ if match is None :
1014+ raise ValueError (
1015+ f"variation_key '{ variation_key } ' not found in agent config '{ agent_key } '"
1016+ )
1017+ raw_variation = match
1018+ else :
1019+ # variation() returns the raw JSON before chevron.render(), so instructions
1020+ # still contain {{placeholder}} tokens rather than empty strings.
1021+ raw_variation = self ._ldClient ._client .variation (agent_key , context , {})
9981022 raw_instructions = raw_variation .get (
9991023 "instructions" , agent_config .instructions
10001024 )
@@ -1030,20 +1054,20 @@ def _fetch_model_configs(
10301054 self ,
10311055 project_key : Optional [str ],
10321056 base_url : Optional [str ],
1033- judges : Optional [Dict [ str , "OptimizationJudge" ] ],
1057+ token_optimization : Optional [bool ],
10341058 ) -> None :
10351059 """Populate ``_model_configs`` from the LD API when credentials are available.
10361060
10371061 When an API key and project key are both present, fetches the model pricing
10381062 catalogue so that ``estimate_cost`` can produce USD figures and the cost gate
10391063 can make meaningful comparisons. If either is absent, ``_model_configs`` is
1040- reset to an empty list and a warning is emitted when cost judges are in use —
1041- cost optimization will silently pass rather than blocking the run .
1064+ reset to an empty list and a warning is emitted when token_optimization is
1065+ enabled — cost data will be unavailable and the cost gate will pass unconditionally .
10421066
10431067 :param project_key: LaunchDarkly project key, or None if not provided.
10441068 :param base_url: Optional API base URL override.
1045- :param judges: Judge map from the caller's options, used only to decide
1046- whether a cost-related warning is appropriate.
1069+ :param token_optimization: Whether token/cost optimization is enabled; used only to
1070+ decide whether a cost-related warning is appropriate.
10471071 """
10481072 self ._model_configs = []
10491073 if self ._has_api_key and project_key :
@@ -1056,9 +1080,9 @@ def _fetch_model_configs(
10561080 self ._model_configs = api_client .get_model_configs (project_key )
10571081 except Exception as exc :
10581082 logger .debug ("Could not pre-fetch model configs: %s" , exc )
1059- elif _acceptance_criteria_implies_cost_optimization ( judges or {}) :
1083+ elif token_optimization :
10601084 logger .warning (
1061- "Cost optimization requires LAUNCHDARKLY_API_KEY and project_key to be set; "
1085+ "Token optimization requires LAUNCHDARKLY_API_KEY and project_key to be set; "
10621086 "cost data will not be available and the cost gate will pass unconditionally"
10631087 )
10641088
@@ -1080,10 +1104,24 @@ async def optimize_from_options(
10801104 raise ValueError (
10811105 "auto_commit requires project_key to be set on OptimizationOptions"
10821106 )
1107+ if options .variation_key :
1108+ if not self ._has_api_key :
1109+ raise ValueError (
1110+ "variation_key requires LAUNCHDARKLY_API_KEY to be set"
1111+ )
1112+ if not options .project_key :
1113+ raise ValueError (
1114+ "variation_key requires project_key to be set on OptimizationOptions"
1115+ )
10831116 self ._agent_key = agent_key
1084- self ._fetch_model_configs (options .project_key , options .base_url , options .judges )
1117+ self ._fetch_model_configs (options .project_key , options .base_url , options .token_optimization )
10851118 context = random .choice (options .context_choices )
1086- agent_config = await self ._get_agent_config (agent_key , context )
1119+ agent_config = await self ._get_agent_config (
1120+ agent_key , context ,
1121+ variation_key = options .variation_key ,
1122+ project_key = options .project_key ,
1123+ base_url = options .base_url ,
1124+ )
10871125 result = await self ._run_optimization (agent_config , options )
10881126 if options .auto_commit and self ._last_run_succeeded and self ._last_succeeded_context :
10891127 self ._commit_variation (
@@ -1119,10 +1157,24 @@ async def optimize_from_ground_truth_options(
11191157 raise ValueError (
11201158 "auto_commit requires project_key to be set on GroundTruthOptimizationOptions"
11211159 )
1160+ if options .variation_key :
1161+ if not self ._has_api_key :
1162+ raise ValueError (
1163+ "variation_key requires LAUNCHDARKLY_API_KEY to be set"
1164+ )
1165+ if not options .project_key :
1166+ raise ValueError (
1167+ "variation_key requires project_key to be set on GroundTruthOptimizationOptions"
1168+ )
11221169 self ._agent_key = agent_key
1123- self ._fetch_model_configs (options .project_key , options .base_url , options .judges )
1170+ self ._fetch_model_configs (options .project_key , options .base_url , options .token_optimization )
11241171 context = random .choice (options .context_choices )
1125- agent_config = await self ._get_agent_config (agent_key , context )
1172+ agent_config = await self ._get_agent_config (
1173+ agent_key , context ,
1174+ variation_key = options .variation_key ,
1175+ project_key = options .project_key ,
1176+ base_url = options .base_url ,
1177+ )
11261178 result = await self ._run_ground_truth_optimization (agent_config , options )
11271179 if options .auto_commit and self ._last_run_succeeded and self ._last_succeeded_context :
11281180 self ._commit_variation (
@@ -1162,6 +1214,8 @@ async def _run_ground_truth_optimization(
11621214 on_failing_result = gt_options .on_failing_result ,
11631215 on_status_update = gt_options .on_status_update ,
11641216 token_limit = gt_options .token_limit ,
1217+ latency_optimization = gt_options .latency_optimization ,
1218+ token_optimization = gt_options .token_optimization ,
11651219 )
11661220 self ._options = bridge
11671221 self ._agent_config = agent_config
@@ -1579,12 +1633,8 @@ async def _generate_new_variation(
15791633 )
15801634 self ._safe_status_update ("generating variation" , status_ctx , iteration )
15811635
1582- optimize_for_duration = _acceptance_criteria_implies_duration_optimization (
1583- self ._options .judges
1584- )
1585- optimize_for_cost = _acceptance_criteria_implies_cost_optimization (
1586- self ._options .judges
1587- )
1636+ optimize_for_duration = bool (self ._options .latency_optimization )
1637+ optimize_for_cost = bool (self ._options .token_optimization )
15881638 quality_already_passing = self ._all_judges_passing ()
15891639 instructions = build_new_variation_prompt (
15901640 self ._history ,
@@ -1989,6 +2039,9 @@ def _persist_and_forward(
19892039 on_failing_result = options .on_failing_result ,
19902040 on_status_update = _persist_and_forward ,
19912041 token_limit = config .get ("tokenLimit" ),
2042+ latency_optimization = config .get ("latencyOptimization" ),
2043+ token_optimization = config .get ("tokenOptimization" ),
2044+ auto_commit = config .get ("autoCommit" , True ),
19922045 )
19932046
19942047 variable_choices : List [Dict [str , Any ]] = config ["variableChoices" ] or [{}]
@@ -2009,6 +2062,9 @@ def _persist_and_forward(
20092062 on_failing_result = options .on_failing_result ,
20102063 on_status_update = _persist_and_forward ,
20112064 token_limit = config .get ("tokenLimit" ),
2065+ latency_optimization = config .get ("latencyOptimization" ),
2066+ token_optimization = config .get ("tokenOptimization" ),
2067+ auto_commit = config .get ("autoCommit" , True ),
20122068 )
20132069
20142070 async def _execute_agent_turn (
@@ -2269,7 +2325,7 @@ def _apply_duration_gate(
22692325 :param ctx: Current optimization context.
22702326 :return: (passed, updated_ctx) where passed reflects gate outcome.
22712327 """
2272- if not _acceptance_criteria_implies_duration_optimization (self ._options .judges ):
2328+ if not bool (self ._options .latency_optimization ):
22732329 return passed_so_far , ctx
22742330 passed = self ._evaluate_duration (ctx )
22752331 if passed :
@@ -2323,7 +2379,7 @@ def _apply_cost_gate(
23232379 :param ctx: Current optimization context.
23242380 :return: (passed, updated_ctx) where passed reflects gate outcome.
23252381 """
2326- if not _acceptance_criteria_implies_cost_optimization (self ._options .judges ):
2382+ if not bool (self ._options .token_optimization ):
23272383 return passed_so_far , ctx
23282384 passed = self ._evaluate_cost (ctx )
23292385 if passed :
0 commit comments