Skip to content

Commit 5c4181c

Browse files
jsonbaileyclaude
andcommitted
feat!: Add ManagedResult, RunnerResult, and Runner protocol; rename invoke() to run()
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 1e1f36b commit 5c4181c

9 files changed

Lines changed: 406 additions & 121 deletions

File tree

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/managed_agent.py

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

3+
from typing import Union
4+
35
from ldai.models import AIAgentConfig
46
from ldai.providers import AgentResult, AgentRunner
7+
from ldai.providers.runner import Runner
8+
from ldai.providers.types import ManagedResult, RunnerResult
59

610

711
class ManagedAgent:
812
"""
913
LaunchDarkly managed wrapper for AI agent invocations.
1014
11-
Holds an AgentRunner. Handles tracking automatically via ``create_tracker()``.
15+
Holds an AgentRunner or Runner. Handles tracking automatically via
16+
``create_tracker()``.
1217
Obtain an instance via ``LDAIClient.create_agent()``.
1318
"""
1419

1520
def __init__(
1621
self,
1722
ai_config: AIAgentConfig,
18-
agent_runner: AgentRunner,
23+
agent_runner: Union[Runner, AgentRunner],
1924
):
2025
self._ai_config = ai_config
2126
self._agent_runner = agent_runner
2227

23-
async def run(self, input: str) -> AgentResult:
28+
async def run(self, input: str) -> ManagedResult:
2429
"""
2530
Run the agent with the given input string.
2631
2732
:param input: The user prompt or input to the agent
28-
:return: AgentResult containing the agent's output and metrics
33+
:return: ManagedResult containing the agent's output and metric summary
2934
"""
3035
tracker = self._ai_config.create_tracker()
31-
return await tracker.track_metrics_of_async(
32-
lambda result: result.metrics,
36+
result: Union[RunnerResult, AgentResult] = await tracker.track_metrics_of_async(
37+
lambda r: r.metrics,
3338
lambda: self._agent_runner.run(input),
3439
)
40+
# Support both RunnerResult (content) and legacy AgentResult (output)
41+
content = result.content if isinstance(result, RunnerResult) else result.output # type: ignore[union-attr]
42+
return ManagedResult(
43+
content=content,
44+
metrics=tracker.get_summary(),
45+
raw=result.raw,
46+
)
3547

36-
def get_agent_runner(self) -> AgentRunner:
48+
def get_agent_runner(self) -> Union[Runner, AgentRunner]:
3749
"""
38-
Return the underlying AgentRunner for advanced use.
50+
Return the underlying runner for advanced use.
3951
40-
:return: The AgentRunner instance.
52+
:return: The Runner or AgentRunner instance.
4153
"""
4254
return self._agent_runner
4355

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

Lines changed: 83 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,113 @@
11
import asyncio
2-
from typing import List, Optional
2+
import warnings
3+
from typing import List, Union
34

45
from ldai import log
56
from ldai.models import AICompletionConfig, LDMessage
67
from ldai.providers.model_runner import ModelRunner
7-
from ldai.providers.types import JudgeResult, ModelResponse
8+
from ldai.providers.runner import Runner
9+
from ldai.providers.types import JudgeResult, ManagedResult, ModelResponse, RunnerResult
810
from ldai.tracker import LDAIConfigTracker
911

1012

1113
class ManagedModel:
1214
"""
1315
LaunchDarkly managed wrapper for AI model invocations.
1416
15-
Holds a ModelRunner. Handles conversation management, judge evaluation
16-
dispatch, and tracking automatically via ``create_tracker()``.
17+
Holds a Runner (or legacy ModelRunner). Handles conversation management,
18+
judge evaluation dispatch, and tracking automatically via ``create_tracker()``.
1719
Obtain an instance via ``LDAIClient.create_model()``.
1820
"""
1921

2022
def __init__(
2123
self,
2224
ai_config: AICompletionConfig,
23-
model_runner: ModelRunner,
25+
model_runner: Union[Runner, ModelRunner],
2426
):
2527
self._ai_config = ai_config
2628
self._model_runner = model_runner
2729
self._messages: List[LDMessage] = []
2830

29-
async def invoke(self, prompt: str) -> ModelResponse:
31+
async def run(self, prompt: str) -> ManagedResult:
3032
"""
31-
Invoke the model with a prompt string.
33+
Run the model with a prompt string.
3234
3335
Appends the prompt to the conversation history, prepends any
3436
system messages from the config, delegates to the runner, and
3537
appends the response to the history.
3638
39+
:param prompt: The user prompt to send to the model
40+
:return: ManagedResult containing the model's response, metric summary,
41+
and an optional evaluations task
42+
"""
43+
tracker = self._ai_config.create_tracker()
44+
45+
user_message = LDMessage(role='user', content=prompt)
46+
self._messages.append(user_message)
47+
48+
config_messages = self._ai_config.messages or []
49+
all_messages = config_messages + self._messages
50+
51+
result: Union[RunnerResult, ModelResponse] = await tracker.track_metrics_of_async(
52+
lambda r: r.metrics,
53+
lambda: self._invoke_runner(all_messages),
54+
)
55+
56+
# Support both new RunnerResult and legacy ModelResponse
57+
if isinstance(result, RunnerResult):
58+
content = result.content
59+
raw = result.raw
60+
parsed = result.parsed
61+
assistant_message = LDMessage(role='assistant', content=content)
62+
else:
63+
content = result.message.content
64+
raw = getattr(result, 'raw', None)
65+
parsed = getattr(result, 'parsed', None)
66+
assistant_message = result.message
67+
68+
input_text = '\r\n'.join(m.content for m in self._messages) if self._messages else ''
69+
70+
evaluations_task = self._track_judge_results(tracker, input_text, content)
71+
72+
self._messages.append(assistant_message)
73+
74+
return ManagedResult(
75+
content=content,
76+
metrics=tracker.get_summary(),
77+
raw=raw,
78+
parsed=parsed,
79+
evaluations=evaluations_task,
80+
)
81+
82+
async def _invoke_runner(
83+
self, all_messages: List[LDMessage]
84+
) -> Union[RunnerResult, ModelResponse]:
85+
"""
86+
Delegate to the runner. Supports both the new ``Runner`` protocol
87+
(``run(messages) → RunnerResult``) and the legacy ``ModelRunner``
88+
(``invoke_model(messages) → ModelResponse``).
89+
"""
90+
if isinstance(self._model_runner, Runner):
91+
return await self._model_runner.run(all_messages)
92+
# Legacy ModelRunner path
93+
return await self._model_runner.invoke_model(all_messages) # type: ignore[union-attr]
94+
95+
async def invoke(self, prompt: str) -> ModelResponse:
96+
"""
97+
Invoke the model with a prompt string.
98+
99+
.. deprecated::
100+
Use :meth:`run` instead. This method will be removed in a future
101+
release once the migration to :class:`ManagedResult` is complete.
102+
37103
:param prompt: The user prompt to send to the model
38104
:return: ModelResponse containing the model's response and metrics
39105
"""
106+
warnings.warn(
107+
"ManagedModel.invoke() is deprecated. Use run() instead.",
108+
DeprecationWarning,
109+
stacklevel=2,
110+
)
40111
tracker = self._ai_config.create_tracker()
41112

42113
user_message = LDMessage(role='user', content=prompt)
@@ -45,9 +116,9 @@ async def invoke(self, prompt: str) -> ModelResponse:
45116
config_messages = self._ai_config.messages or []
46117
all_messages = config_messages + self._messages
47118

48-
response = await tracker.track_metrics_of_async(
119+
response: ModelResponse = await tracker.track_metrics_of_async(
49120
lambda result: result.metrics,
50-
lambda: self._model_runner.invoke_model(all_messages),
121+
lambda: self._model_runner.invoke_model(all_messages), # type: ignore[union-attr]
51122
)
52123

53124
input_text = '\r\n'.join(m.content for m in self._messages) if self._messages else ''
@@ -98,11 +169,11 @@ def append_messages(self, messages: List[LDMessage]) -> None:
98169
"""
99170
self._messages.extend(messages)
100171

101-
def get_model_runner(self) -> ModelRunner:
172+
def get_model_runner(self) -> Union[Runner, ModelRunner]:
102173
"""
103-
Return the underlying ModelRunner for advanced use.
174+
Return the underlying runner for advanced use.
104175
105-
:return: The ModelRunner instance.
176+
:return: The Runner or legacy ModelRunner instance.
106177
"""
107178
return self._model_runner
108179

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: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
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. Both model runners and agent runners implement this protocol.
15+
16+
:param input: The input to the runner (string prompt, list of messages, or
17+
other provider-specific input type).
18+
:param output_type: Optional JSON schema dict that requests structured output.
19+
When provided, the runner populates :attr:`~RunnerResult.parsed` on the
20+
returned :class:`RunnerResult`.
21+
:return: :class:`RunnerResult` containing ``content``, ``metrics``, and
22+
optionally ``raw`` and ``parsed``.
23+
"""
24+
25+
async def run(
26+
self,
27+
input: Any,
28+
output_type: Optional[Dict[str, Any]] = None,
29+
) -> RunnerResult:
30+
"""
31+
Execute the runner with the given input.
32+
33+
:param input: The input to the runner.
34+
:param output_type: Optional JSON schema for structured output.
35+
:return: RunnerResult containing content, metrics, raw, and parsed fields.
36+
"""
37+
...

0 commit comments

Comments
 (0)