Skip to content

Commit 88d4ddc

Browse files
authored
feat!: Add ManagedResult, RunnerResult, and Runner protocol; rename invoke() to run() (#148)
1 parent 86c79e6 commit 88d4ddc

15 files changed

Lines changed: 569 additions & 246 deletions

packages/sdk/server-ai/src/ldai/__init__.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,10 +36,13 @@
3636
AgentGraphRunner,
3737
AgentResult,
3838
AgentRunner,
39+
ManagedResult,
40+
Runner,
41+
RunnerResult,
3942
ToolRegistry,
4043
)
4144
from ldai.providers.types import JudgeResult
42-
from ldai.tracker import AIGraphTracker
45+
from ldai.tracker import AIGraphTracker, LDAIMetricSummary
4346

4447
__all__ = [
4548
'LDAIClient',
@@ -48,6 +51,10 @@
4851
'AgentGraphRunner',
4952
'AgentResult',
5053
'AgentGraphResult',
54+
'ManagedResult',
55+
'Runner',
56+
'RunnerResult',
57+
'LDAIMetricSummary',
5158
'ToolRegistry',
5259
'AIAgentConfig',
5360
'AIAgentConfigDefault',

packages/sdk/server-ai/src/ldai/client.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -411,8 +411,8 @@ async def create_model(
411411
)
412412
413413
if model:
414-
response = await model.invoke("I need help with my order")
415-
print(response.message.content)
414+
response = await model.run("I need help with my order")
415+
print(response.content)
416416
"""
417417
self._client.track(_TRACK_USAGE_CREATE_MODEL, context, key, 1)
418418
log.debug(f"Creating managed model for key: {key}")

packages/sdk/server-ai/src/ldai/judge/__init__.py

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@
88
from ldai import log
99
from ldai.judge.evaluation_schema_builder import EvaluationSchemaBuilder
1010
from ldai.models import AIJudgeConfig, LDMessage
11-
from ldai.providers.model_runner import ModelRunner
12-
from ldai.providers.types import JudgeResult, ModelResponse
11+
from ldai.providers.runner import Runner
12+
from ldai.providers.types import JudgeResult, RunnerResult
1313

1414

1515
class Judge:
@@ -23,7 +23,7 @@ class Judge:
2323
def __init__(
2424
self,
2525
ai_config: AIJudgeConfig,
26-
model_runner: ModelRunner,
26+
model_runner: Runner,
2727
sample_rate: float = 1.0,
2828
):
2929
"""
@@ -82,10 +82,14 @@ async def evaluate(
8282

8383
response = await tracker.track_metrics_of_async(
8484
lambda result: result.metrics,
85-
lambda: self._model_runner.invoke_structured_model(messages, self._evaluation_response_structure),
85+
lambda: self._model_runner.run(messages, output_type=self._evaluation_response_structure),
8686
)
8787

88-
parsed = self._parse_evaluation_response(response.data)
88+
if response.parsed is None:
89+
log.warning('Judge evaluation did not return structured output')
90+
return judge_result
91+
92+
parsed = self._parse_evaluation_response(response.parsed)
8993

9094
if parsed is None:
9195
log.warning('Judge evaluation did not return the expected evaluation')
@@ -105,20 +109,20 @@ async def evaluate(
105109
async def evaluate_messages(
106110
self,
107111
messages: list[LDMessage],
108-
response: ModelResponse,
112+
response: RunnerResult,
109113
sampling_ratio: Optional[float] = None,
110114
) -> JudgeResult:
111115
"""
112116
Evaluates an AI response from chat messages and response.
113117
114118
:param messages: Array of messages representing the conversation history
115-
:param response: The AI response to be evaluated
119+
:param response: The runner result to be evaluated
116120
:param sampling_ratio: Sampling ratio (0-1) to determine if evaluation should be processed.
117121
When ``None`` (the default), falls back to ``self.sample_rate``.
118122
:return: The result of the judge evaluation.
119123
"""
120124
input_text = '\r\n'.join([msg.content for msg in messages]) if messages else ''
121-
output_text = response.message.content
125+
output_text = response.content
122126

123127
return await self.evaluate(input_text, output_text, sampling_ratio)
124128

@@ -130,7 +134,7 @@ def get_ai_config(self) -> AIJudgeConfig:
130134
"""
131135
return self._ai_config
132136

133-
def get_model_runner(self) -> ModelRunner:
137+
def get_model_runner(self) -> Runner:
134138
"""
135139
Returns the model runner used by this judge.
136140

packages/sdk/server-ai/src/ldai/managed_agent.py

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,49 @@
11
"""ManagedAgent — LaunchDarkly managed wrapper for agent invocations."""
22

33
from ldai.models import AIAgentConfig
4-
from ldai.providers import AgentResult, AgentRunner
4+
from ldai.providers.runner import Runner
5+
from ldai.providers.types import ManagedResult
56

67

78
class ManagedAgent:
89
"""
910
LaunchDarkly managed wrapper for AI agent invocations.
1011
11-
Holds an AgentRunner. Handles tracking automatically via ``create_tracker()``.
12+
Holds a Runner. Handles tracking automatically via ``create_tracker()``.
1213
Obtain an instance via ``LDAIClient.create_agent()``.
1314
"""
1415

1516
def __init__(
1617
self,
1718
ai_config: AIAgentConfig,
18-
agent_runner: AgentRunner,
19+
agent_runner: Runner,
1920
):
2021
self._ai_config = ai_config
2122
self._agent_runner = agent_runner
2223

23-
async def run(self, input: str) -> AgentResult:
24+
async def run(self, input: str) -> ManagedResult:
2425
"""
2526
Run the agent with the given input string.
2627
2728
:param input: The user prompt or input to the agent
28-
:return: AgentResult containing the agent's output and metrics
29+
:return: ManagedResult containing the agent's output and metric summary
2930
"""
3031
tracker = self._ai_config.create_tracker()
31-
return await tracker.track_metrics_of_async(
32-
lambda result: result.metrics,
32+
result = await tracker.track_metrics_of_async(
33+
lambda r: r.metrics,
3334
lambda: self._agent_runner.run(input),
3435
)
36+
return ManagedResult(
37+
content=result.content,
38+
metrics=tracker.get_summary(),
39+
raw=result.raw,
40+
)
3541

36-
def get_agent_runner(self) -> AgentRunner:
42+
def get_agent_runner(self) -> Runner:
3743
"""
38-
Return the underlying AgentRunner for advanced use.
44+
Return the underlying runner for advanced use.
3945
40-
:return: The AgentRunner instance.
46+
:return: The Runner instance.
4147
"""
4248
return self._agent_runner
4349

packages/sdk/server-ai/src/ldai/managed_model.py

Lines changed: 28 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,42 @@
11
import asyncio
2-
from typing import List, Optional
2+
from typing import List
33

44
from ldai import log
55
from ldai.models import AICompletionConfig, LDMessage
6-
from ldai.providers.model_runner import ModelRunner
7-
from ldai.providers.types import JudgeResult, ModelResponse
6+
from ldai.providers.runner import Runner
7+
from ldai.providers.types import JudgeResult, ManagedResult
88
from ldai.tracker import LDAIConfigTracker
99

1010

1111
class ManagedModel:
1212
"""
1313
LaunchDarkly managed wrapper for AI model invocations.
1414
15-
Holds a ModelRunner. Handles conversation management, judge evaluation
15+
Holds a Runner. Handles conversation management, judge evaluation
1616
dispatch, and tracking automatically via ``create_tracker()``.
1717
Obtain an instance via ``LDAIClient.create_model()``.
1818
"""
1919

2020
def __init__(
2121
self,
2222
ai_config: AICompletionConfig,
23-
model_runner: ModelRunner,
23+
model_runner: Runner,
2424
):
2525
self._ai_config = ai_config
2626
self._model_runner = model_runner
2727
self._messages: List[LDMessage] = []
2828

29-
async def invoke(self, prompt: str) -> ModelResponse:
29+
async def run(self, prompt: str) -> ManagedResult:
3030
"""
31-
Invoke the model with a prompt string.
31+
Run the model with a prompt string.
3232
3333
Appends the prompt to the conversation history, prepends any
3434
system messages from the config, delegates to the runner, and
3535
appends the response to the history.
3636
3737
:param prompt: The user prompt to send to the model
38-
:return: ModelResponse containing the model's response and metrics
38+
:return: ManagedResult containing the model's response, metric summary,
39+
and an optional evaluations task
3940
"""
4041
tracker = self._ai_config.create_tracker()
4142

@@ -45,17 +46,26 @@ async def invoke(self, prompt: str) -> ModelResponse:
4546
config_messages = self._ai_config.messages or []
4647
all_messages = config_messages + self._messages
4748

48-
response = await tracker.track_metrics_of_async(
49-
lambda result: result.metrics,
50-
lambda: self._model_runner.invoke_model(all_messages),
49+
result = await tracker.track_metrics_of_async(
50+
lambda r: r.metrics,
51+
lambda: self._model_runner.run(all_messages),
5152
)
5253

54+
assistant_message = LDMessage(role='assistant', content=result.content)
55+
5356
input_text = '\r\n'.join(m.content for m in self._messages) if self._messages else ''
54-
output_text = response.message.content
55-
response.evaluations = self._track_judge_results(tracker, input_text, output_text)
5657

57-
self._messages.append(response.message)
58-
return response
58+
evaluations_task = self._track_judge_results(tracker, input_text, result.content)
59+
60+
self._messages.append(assistant_message)
61+
62+
return ManagedResult(
63+
content=result.content,
64+
metrics=tracker.get_summary(),
65+
raw=result.raw,
66+
parsed=result.parsed,
67+
evaluations=evaluations_task,
68+
)
5969

6070
def _track_judge_results(
6171
self,
@@ -98,11 +108,11 @@ def append_messages(self, messages: List[LDMessage]) -> None:
98108
"""
99109
self._messages.extend(messages)
100110

101-
def get_model_runner(self) -> ModelRunner:
111+
def get_model_runner(self) -> Runner:
102112
"""
103-
Return the underlying ModelRunner for advanced use.
113+
Return the underlying runner for advanced use.
104114
105-
:return: The ModelRunner instance.
115+
:return: The Runner instance.
106116
"""
107117
return self._model_runner
108118

packages/sdk/server-ai/src/ldai/providers/__init__.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,16 @@
22
from ldai.providers.agent_runner import AgentRunner
33
from ldai.providers.ai_provider import AIProvider
44
from ldai.providers.model_runner import ModelRunner
5+
from ldai.providers.runner import Runner
56
from ldai.providers.runner_factory import RunnerFactory
67
from ldai.providers.types import (
78
AgentGraphResult,
89
AgentResult,
910
JudgeResult,
1011
LDAIMetrics,
12+
ManagedResult,
1113
ModelResponse,
14+
RunnerResult,
1215
StructuredResponse,
1316
ToolRegistry,
1417
)
@@ -21,9 +24,12 @@
2124
'AgentRunner',
2225
'JudgeResult',
2326
'LDAIMetrics',
27+
'ManagedResult',
2428
'ModelResponse',
2529
'ModelRunner',
30+
'Runner',
2631
'RunnerFactory',
32+
'RunnerResult',
2733
'StructuredResponse',
2834
'ToolRegistry',
2935
]
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
"""Unified Runner protocol for AI providers."""
2+
3+
from typing import Any, Dict, Optional, Protocol, runtime_checkable
4+
5+
from ldai.providers.types import RunnerResult
6+
7+
8+
@runtime_checkable
9+
class Runner(Protocol):
10+
"""
11+
Unified runtime capability interface for all AI provider runners.
12+
13+
A :class:`Runner` is a focused, configured object that performs a single
14+
AI invocation.
15+
"""
16+
17+
async def run(
18+
self,
19+
input: Any,
20+
output_type: Optional[Dict[str, Any]] = None,
21+
) -> RunnerResult:
22+
"""
23+
Execute the runner with the given input.
24+
25+
:param input: The input to the runner.
26+
:param output_type: Optional JSON schema for structured output.
27+
:return: RunnerResult containing content, metrics, raw, and parsed fields.
28+
"""
29+
...

packages/sdk/server-ai/src/ldai/providers/runner_factory.py

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,8 @@
44
from ldai import log
55
from ldai.models import AIConfigKind
66
from ldai.providers.agent_graph_runner import AgentGraphRunner
7-
from ldai.providers.agent_runner import AgentRunner
87
from ldai.providers.ai_provider import AIProvider
9-
from ldai.providers.model_runner import ModelRunner
8+
from ldai.providers.runner import Runner
109

1110
T = TypeVar('T')
1211

@@ -118,13 +117,13 @@ def _get_providers_to_try(
118117
def create_model(
119118
config: AIConfigKind,
120119
default_ai_provider: Optional[str] = None,
121-
) -> Optional[ModelRunner]:
120+
) -> Optional[Runner]:
122121
"""
123122
Create a model executor for the given AI completion config.
124123
125124
:param config: LaunchDarkly AI config (completion or judge)
126125
:param default_ai_provider: Optional provider override ('openai', 'langchain', …)
127-
:return: Configured ModelRunner ready to invoke the model, or None
126+
:return: Configured Runner ready to invoke the model, or None
128127
"""
129128
provider_name = config.provider.name.lower() if config.provider else None
130129
providers = RunnerFactory._get_providers_to_try(default_ai_provider, provider_name)
@@ -135,7 +134,7 @@ def create_agent(
135134
config: Any,
136135
tools: Any,
137136
default_ai_provider: Optional[str] = None,
138-
) -> Optional[AgentRunner]:
137+
) -> Optional[Runner]:
139138
"""
140139
CAUTION:
141140
This feature is experimental and should NOT be considered ready for production use.
@@ -147,7 +146,7 @@ def create_agent(
147146
:param config: LaunchDarkly AI agent config
148147
:param tools: Tool registry mapping tool names to callables
149148
:param default_ai_provider: Optional provider override
150-
:return: AgentRunner instance, or None
149+
:return: Runner instance, or None
151150
"""
152151
provider_name = config.provider.name.lower() if config.provider else None
153152
providers = RunnerFactory._get_providers_to_try(default_ai_provider, provider_name)

0 commit comments

Comments
 (0)