Skip to content

Commit c5e39eb

Browse files
committed
feat: ability to specify variation key on config or from_options for optimization package
1 parent e8c6692 commit c5e39eb

5 files changed

Lines changed: 499 additions & 14 deletions

File tree

packages/optimization/src/ldai_optimizer/client.py

Lines changed: 80 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -824,37 +824,68 @@ async def _evaluate_acceptance_judge(
824824
return dataclasses.replace(judge_result, duration_ms=judge_duration_ms, usage=judge_response.usage)
825825

826826
async def _get_agent_config(
827-
self, agent_key: str, context: Context
827+
self,
828+
agent_key: str,
829+
context: Context,
830+
variation_key: Optional[str] = None,
831+
project_key: Optional[str] = None,
832+
api_client: Optional["LDApiClient"] = None,
833+
base_url: Optional[str] = None,
828834
) -> AIAgentConfig:
829835
"""
830836
Fetch the agent configuration, replacing the instructions with the raw variation
831837
template so that {{placeholder}} tokens are preserved for client-side interpolation.
832838
833839
agent_config() is called normally so we get a fully populated AIAgentConfig
834-
(including the tracker). We then call variation() separately to retrieve the
835-
unrendered instruction template and swap it in, keeping everything else intact.
840+
(including the tracker). When variation_key is set, the specific variation's
841+
data (instructions, model, tools) is fetched via the REST API and used as the
842+
base instead of the SDK-evaluated default. Otherwise, variation() is called to
843+
retrieve the unrendered instruction template for the SDK-evaluated variation.
836844
837845
:param agent_key: The key for the agent to get the configuration for
838846
:param context: The evaluation context
847+
:param variation_key: If set, fetch this specific variation from the API as the base.
848+
:param project_key: Required when variation_key is set.
849+
:param api_client: Optional pre-built LDApiClient to reuse (e.g. from optimize_from_config).
850+
:param base_url: Optional base URL override for a newly created LDApiClient.
839851
:return: AIAgentConfig with raw {{placeholder}} instruction templates intact
840852
"""
841853
try:
842854
agent_config = self._ldClient.agent_config(agent_key, context)
843855

844-
# variation() returns the raw JSON before chevron.render(), so instructions
845-
# still contain {{placeholder}} tokens rather than empty strings.
846-
raw_variation = self._ldClient._client.variation(agent_key, context, {})
847-
raw_instructions = raw_variation.get(
848-
"instructions", agent_config.instructions
849-
)
856+
if variation_key:
857+
# Fetch the specific variation from the REST API so instructions,
858+
# model, and tools all come from the requested base variation rather
859+
# than whatever the SDK evaluates for the given context.
860+
client = api_client or LDApiClient(
861+
self._api_key, # type: ignore[arg-type]
862+
**({"base_url": base_url} if base_url else {}),
863+
)
864+
variation_data = client.get_ai_config_variation(project_key, agent_key, variation_key) # type: ignore[arg-type]
865+
raw_instructions = variation_data.get("instructions") or ""
866+
raw_tools = variation_data.get("tools") or []
867+
model_config_key = variation_data.get("modelConfigKey") or ""
868+
if model_config_key:
869+
agent_config = dataclasses.replace(
870+
agent_config,
871+
model=ModelConfig(name=model_config_key, parameters={}),
872+
)
873+
else:
874+
# variation() returns the raw JSON before chevron.render(), so instructions
875+
# still contain {{placeholder}} tokens rather than empty strings.
876+
raw_variation = self._ldClient._client.variation(agent_key, context, {})
877+
raw_instructions = raw_variation.get(
878+
"instructions", agent_config.instructions
879+
)
880+
raw_tools = raw_variation.get("tools", [])
881+
850882
if not raw_instructions:
851883
raise ValueError(
852884
f"Agent '{agent_key}' has no instructions configured. "
853885
"Ensure the agent flag has instructions set before running an optimization."
854886
)
855887
self._initial_instructions = raw_instructions
856888

857-
raw_tools = raw_variation.get("tools", [])
858889
self._initial_tool_keys = [
859890
t["key"]
860891
for t in raw_tools
@@ -888,9 +919,24 @@ async def optimize_from_options(
888919
raise ValueError(
889920
"auto_commit requires project_key to be set on OptimizationOptions"
890921
)
922+
if options.variation_key:
923+
if not self._has_api_key:
924+
raise ValueError(
925+
"variation_key requires LAUNCHDARKLY_API_KEY to be set"
926+
)
927+
if not options.project_key:
928+
raise ValueError(
929+
"variation_key requires project_key to be set on OptimizationOptions"
930+
)
891931
self._agent_key = agent_key
892932
context = random.choice(options.context_choices)
893-
agent_config = await self._get_agent_config(agent_key, context)
933+
agent_config = await self._get_agent_config(
934+
agent_key,
935+
context,
936+
variation_key=options.variation_key,
937+
project_key=options.project_key,
938+
base_url=options.base_url,
939+
)
894940
result = await self._run_optimization(agent_config, options)
895941
if options.auto_commit and self._last_run_succeeded and self._last_succeeded_context:
896942
self._commit_variation(
@@ -926,9 +972,24 @@ async def optimize_from_ground_truth_options(
926972
raise ValueError(
927973
"auto_commit requires project_key to be set on GroundTruthOptimizationOptions"
928974
)
975+
if options.variation_key:
976+
if not self._has_api_key:
977+
raise ValueError(
978+
"variation_key requires LAUNCHDARKLY_API_KEY to be set"
979+
)
980+
if not options.project_key:
981+
raise ValueError(
982+
"variation_key requires project_key to be set on GroundTruthOptimizationOptions"
983+
)
929984
self._agent_key = agent_key
930985
context = random.choice(options.context_choices)
931-
agent_config = await self._get_agent_config(agent_key, context)
986+
agent_config = await self._get_agent_config(
987+
agent_key,
988+
context,
989+
variation_key=options.variation_key,
990+
project_key=options.project_key,
991+
base_url=options.base_url,
992+
)
932993
result = await self._run_ground_truth_optimization(agent_config, options)
933994
if options.auto_commit and self._last_run_succeeded and self._last_succeeded_context:
934995
self._commit_variation(
@@ -1425,7 +1486,13 @@ async def optimize_from_config(
14251486
context = random.choice(options.context_choices)
14261487
# _get_agent_config calls _initialize_class_members_from_config internally;
14271488
# _run_optimization calls it again to reset history before the loop starts.
1428-
agent_config = await self._get_agent_config(self._agent_key, context)
1489+
agent_config = await self._get_agent_config(
1490+
self._agent_key,
1491+
context,
1492+
variation_key=config.get("variationKey"),
1493+
project_key=options.project_key,
1494+
api_client=api_client,
1495+
)
14291496

14301497
optimization_options = self._build_options_from_config(
14311498
config, options, api_client, optimization_key, run_id, model_configs

packages/optimization/src/ldai_optimizer/dataclasses.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -342,6 +342,9 @@ class OptimizationOptions:
342342
project_key: Optional[str] = None # required when auto_commit=True
343343
output_key: Optional[str] = None # variation key/name; auto-generated if omitted
344344
base_url: Optional[str] = None # override to target a non-default LD instance
345+
# When set, uses this specific variation as the base instead of the SDK-evaluated default.
346+
# Requires LAUNCHDARKLY_API_KEY to be set and project_key to be provided.
347+
variation_key: Optional[str] = None
345348
on_passing_result: Optional[Callable[[OptimizationContext], None]] = None
346349
on_failing_result: Optional[Callable[[OptimizationContext], None]] = None
347350
# called to provide status updates during the optimization flow
@@ -434,6 +437,9 @@ class GroundTruthOptimizationOptions:
434437
project_key: Optional[str] = None # required when auto_commit=True
435438
output_key: Optional[str] = None # variation key/name; auto-generated if omitted
436439
base_url: Optional[str] = None # override to target a non-default LD instance
440+
# When set, uses this specific variation as the base instead of the SDK-evaluated default.
441+
# Requires LAUNCHDARKLY_API_KEY to be set and project_key to be provided.
442+
variation_key: Optional[str] = None
437443
token_limit: Optional[int] = None # stop the run when total token usage reaches this value
438444

439445
def __post_init__(self):

packages/optimization/src/ldai_optimizer/ld_api_client.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ class AgentOptimizationConfig(_AgentOptimizationConfigRequired, total=False):
9090
groundTruthResponses: List[str]
9191
metricKey: str
9292
tokenLimit: int
93+
variationKey: str
9394

9495

9596
# ---------------------------------------------------------------------------
@@ -287,6 +288,29 @@ def get_ai_config(self, project_key: str, config_key: str) -> Any:
287288
path = f"/api/v2/projects/{project_key}/ai-configs/{config_key}"
288289
return self._request("GET", path, extra_headers={"LD-API-Version": "beta"})
289290

291+
def get_ai_config_variation(
292+
self, project_key: str, config_key: str, variation_key: str
293+
) -> Dict[str, Any]:
294+
"""Fetch a specific variation of an AI config by key.
295+
296+
Returns the first (latest) item from the variations response.
297+
298+
:param project_key: LaunchDarkly project key.
299+
:param config_key: Key of the AI Config (aiConfigKey).
300+
:param variation_key: Key of the specific variation to fetch.
301+
:return: The variation dict (first item from the ``items`` array).
302+
:raises LDApiError: If the variation is not found or the request fails.
303+
"""
304+
path = f"/api/v2/projects/{project_key}/ai-configs/{config_key}/variations/{variation_key}"
305+
result = self._request("GET", path, extra_headers={"LD-API-Version": "beta"})
306+
items = result.get("items") if isinstance(result, dict) else None
307+
if not items:
308+
raise LDApiError(
309+
f"Variation '{variation_key}' not found for AI config '{config_key}'.",
310+
path=path,
311+
)
312+
return items[0]
313+
290314
def create_ai_config_variation(
291315
self, project_key: str, config_key: str, payload: Dict[str, Any]
292316
) -> Any:

0 commit comments

Comments
 (0)