Skip to content

Commit 4319954

Browse files
jsonbaileyclaude
andcommitted
feat: Add ManagedGraphResult, GraphMetricSummary, and AgentGraphRunnerResult types
- Add GraphMetrics dataclass (runner-layer return type for graph runs) - Add GraphMetricSummary dataclass (managed-layer metrics, analogous to LDAIMetricSummary for single-model invocations) - Add ManagedGraphResult dataclass (managed-layer return type from ManagedAgentGraph) - Add AgentGraphRunnerResult dataclass (future runner return type, no evaluations field) - ManagedAgentGraph.run() now returns ManagedGraphResult with GraphMetricSummary built from the runner's AgentGraphResult metrics - Export all new types from ldai package Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 5df809b commit 4319954

6 files changed

Lines changed: 146 additions & 25 deletions

File tree

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,12 @@
3434
from ldai.providers import (
3535
AgentGraphResult,
3636
AgentGraphRunner,
37+
AgentGraphRunnerResult,
3738
AgentResult,
3839
AgentRunner,
40+
GraphMetrics,
41+
GraphMetricSummary,
42+
ManagedGraphResult,
3943
ManagedResult,
4044
Runner,
4145
RunnerResult,
@@ -51,6 +55,10 @@
5155
'AgentGraphRunner',
5256
'AgentResult',
5357
'AgentGraphResult',
58+
'AgentGraphRunnerResult',
59+
'GraphMetrics',
60+
'GraphMetricSummary',
61+
'ManagedGraphResult',
5462
'ManagedResult',
5563
'Runner',
5664
'RunnerResult',

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

Lines changed: 31 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,19 @@
11
"""ManagedAgentGraph — LaunchDarkly managed wrapper for agent graph execution."""
22

3-
from typing import Any
3+
import asyncio
4+
from typing import Any, List
45

56
from ldai.providers import AgentGraphResult, AgentGraphRunner
7+
from ldai.providers.types import GraphMetricSummary, JudgeResult, ManagedGraphResult
68

79

810
class ManagedAgentGraph:
911
"""
1012
LaunchDarkly managed wrapper for AI agent graph execution.
1113
12-
Holds an AgentGraphRunner. Auto-tracking of path,
13-
tool calls, handoffs, latency, and invocation success/failure is handled
14-
by the runner implementation.
14+
Holds an AgentGraphRunner. Wraps the runner result in a
15+
:class:`~ldai.providers.types.ManagedGraphResult` and builds a
16+
:class:`~ldai.providers.types.GraphMetricSummary` from the runner's metrics.
1517
1618
Obtain an instance via ``LDAIClient.create_agent_graph()``.
1719
"""
@@ -27,17 +29,37 @@ def __init__(
2729
"""
2830
self._runner = runner
2931

30-
async def run(self, input: Any) -> AgentGraphResult:
32+
async def run(self, input: Any) -> ManagedGraphResult:
3133
"""
3234
Run the agent graph with the given input.
3335
34-
Delegates to the underlying AgentGraphRunner, which handles
35-
execution and all auto-tracking internally.
36+
Delegates to the underlying AgentGraphRunner, builds a
37+
:class:`GraphMetricSummary` from the result, and wraps everything in a
38+
:class:`ManagedGraphResult`.
3639
3740
:param input: The input prompt or structured input for the graph
38-
:return: AgentGraphResult containing the output, raw response, and metrics
41+
:return: ManagedGraphResult containing the content, metric summary, raw response,
42+
and an optional evaluations task (currently always ``None`` for graphs —
43+
per-graph evaluations will be added in a future PR).
3944
"""
40-
return await self._runner.run(input)
45+
result: AgentGraphResult = await self._runner.run(input)
46+
47+
# Build a GraphMetricSummary from the runner result's LDAIMetrics.
48+
# path and node_metrics will be populated once graph runners are migrated
49+
# to return AgentGraphRunnerResult with GraphMetrics (PR 11).
50+
metrics = result.metrics
51+
summary = GraphMetricSummary(
52+
success=metrics.success,
53+
usage=metrics.usage,
54+
duration_ms=getattr(metrics, 'duration_ms', None),
55+
)
56+
57+
return ManagedGraphResult(
58+
content=result.output,
59+
metrics=summary,
60+
raw=result.raw,
61+
evaluations=None,
62+
)
4163

4264
def get_agent_graph_runner(self) -> AgentGraphRunner:
4365
"""

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,13 @@
66
from ldai.providers.runner_factory import RunnerFactory
77
from ldai.providers.types import (
88
AgentGraphResult,
9+
AgentGraphRunnerResult,
910
AgentResult,
11+
GraphMetrics,
12+
GraphMetricSummary,
1013
JudgeResult,
1114
LDAIMetrics,
15+
ManagedGraphResult,
1216
ManagedResult,
1317
ModelResponse,
1418
RunnerResult,
@@ -20,10 +24,14 @@
2024
'AIProvider',
2125
'AgentGraphResult',
2226
'AgentGraphRunner',
27+
'AgentGraphRunnerResult',
2328
'AgentResult',
2429
'AgentRunner',
30+
'GraphMetrics',
31+
'GraphMetricSummary',
2532
'JudgeResult',
2633
'LDAIMetrics',
34+
'ManagedGraphResult',
2735
'ManagedResult',
2836
'ModelResponse',
2937
'ModelRunner',

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

Lines changed: 82 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from __future__ import annotations
44

55
import asyncio
6-
from dataclasses import dataclass
6+
from dataclasses import dataclass, field
77
from typing import Any, Callable, Dict, List, Optional
88

99
from ldai.models import LDMessage
@@ -87,6 +87,81 @@ class ManagedResult:
8787
evaluations: Optional[asyncio.Task[List[JudgeResult]]] = None
8888

8989

90+
@dataclass
91+
class GraphMetrics:
92+
"""
93+
Metrics from a single agent graph run.
94+
95+
Returned by graph runner implementations (e.g. ``OpenAIAgentGraphRunner``,
96+
``LangGraphAgentGraphRunner``) and consumed by :class:`ManagedAgentGraph` to
97+
build a :class:`GraphMetricSummary`.
98+
99+
``path`` is the ordered list of node keys visited during the run.
100+
``node_metrics`` maps node keys to the :class:`LDAIMetrics` recorded for each
101+
node. Both fields default to empty so that runners that do not yet populate
102+
them remain compatible.
103+
"""
104+
success: bool
105+
path: List[str] = field(default_factory=list)
106+
duration_ms: Optional[int] = None
107+
usage: Optional[TokenUsage] = None
108+
node_metrics: Dict[str, LDAIMetrics] = field(default_factory=dict)
109+
110+
111+
@dataclass
112+
class GraphMetricSummary:
113+
"""
114+
Summary of metrics for an agent graph run, as returned by
115+
:attr:`ManagedGraphResult.metrics`.
116+
117+
Mirrors :class:`LDAIMetricSummary` for single-model invocations but adds
118+
graph-specific fields (``path``, ``node_metrics``).
119+
120+
``resumption_token`` is set from the graph tracker at instantiation so it is
121+
always available on the returned result.
122+
"""
123+
success: bool
124+
path: List[str] = field(default_factory=list)
125+
duration_ms: Optional[int] = None
126+
usage: Optional[TokenUsage] = None
127+
node_metrics: Dict[str, LDAIMetrics] = field(default_factory=dict)
128+
resumption_token: Optional[str] = None
129+
130+
131+
@dataclass
132+
class ManagedGraphResult:
133+
"""
134+
Result returned by :class:`~ldai.ManagedAgentGraph` after a single graph run.
135+
136+
``metrics`` is a :class:`GraphMetricSummary` built from the runner result and
137+
the graph tracker.
138+
``evaluations`` is an optional asyncio Task that resolves to a list of
139+
:class:`JudgeResult` instances when awaited.
140+
"""
141+
content: str
142+
metrics: GraphMetricSummary
143+
raw: Optional[Any] = None
144+
evaluations: Optional[asyncio.Task[List[JudgeResult]]] = None
145+
146+
147+
@dataclass
148+
class AgentGraphRunnerResult:
149+
"""
150+
Result returned by an agent graph runner (e.g. ``OpenAIAgentGraphRunner``).
151+
152+
This is the new runner-layer return type for graph runners. ``evaluations``
153+
is intentionally absent — evaluations are dispatched by the managed layer
154+
and live on :class:`ManagedGraphResult`.
155+
156+
.. note::
157+
Graph runners have not yet been migrated to return this type (that is
158+
PR 11). Until then, :class:`AgentGraphResult` is still in use.
159+
"""
160+
content: str
161+
metrics: GraphMetrics
162+
raw: Optional[Any] = None
163+
164+
90165
@dataclass
91166
class ModelResponse:
92167
"""
@@ -166,6 +241,12 @@ class AgentResult:
166241
class AgentGraphResult:
167242
"""
168243
Result from an agent graph run.
244+
245+
.. deprecated::
246+
Use :class:`AgentGraphRunnerResult` (runner layer) and
247+
:class:`ManagedGraphResult` (managed layer) instead. This type is
248+
retained for backward compatibility with existing graph runners until
249+
PR 11 migrates them to return :class:`AgentGraphRunnerResult`.
169250
"""
170251
output: str
171252
raw: Any

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

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -410,17 +410,6 @@ def track_feedback(self, feedback: Dict[str, FeedbackKind]) -> None:
410410
1,
411411
)
412412

413-
def track_tool_calls(self, tool_calls: List[str]) -> None:
414-
"""
415-
Track the tool calls made during an AI operation.
416-
417-
:param tool_calls: List of tool call names.
418-
"""
419-
if self._summary.tool_calls is not None:
420-
log.warning("Tool calls have already been tracked for this execution. %s", self.__get_track_data())
421-
return
422-
self._summary._tool_calls = list(tool_calls)
423-
424413
def track_success(self) -> None:
425414
"""
426415
Track a successful AI generation.
@@ -560,9 +549,20 @@ def track_tool_calls(self, tool_keys: Iterable[str]) -> None:
560549
"""
561550
Track multiple tool invocations for this configuration.
562551
552+
Records the tool keys on :class:`LDAIMetricSummary` and emits a
553+
``$ld:ai:tool_call`` event for each one.
554+
563555
:param tool_keys: Tool identifiers (e.g. from a model response).
564556
"""
565-
for tool_key in tool_keys:
557+
if self._summary.tool_calls is not None:
558+
log.warning(
559+
"Tool calls have already been tracked for this execution. %s",
560+
self.__get_track_data(),
561+
)
562+
return
563+
keys = list(tool_keys)
564+
self._summary._tool_calls = keys
565+
for tool_key in keys:
566566
self.track_tool_call(tool_key)
567567

568568
def get_summary(self) -> LDAIMetricSummary:

packages/sdk/server-ai/tests/test_managed_agent_graph.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from ldclient import Config, Context, LDClient
66
from ldclient.integrations.test_data import TestData
77

8-
from ldai import LDAIClient, ManagedAgentGraph
8+
from ldai import LDAIClient, ManagedAgentGraph, ManagedGraphResult
99
from ldai.providers.types import LDAIMetrics
1010
from ldai.providers import AgentGraphResult, AgentGraphRunner, ToolRegistry
1111

@@ -31,7 +31,8 @@ async def test_managed_agent_graph_run_delegates_to_runner():
3131
runner = StubAgentGraphRunner("hello world")
3232
managed = ManagedAgentGraph(runner)
3333
result = await managed.run("test input")
34-
assert result.output == "hello world"
34+
assert isinstance(result, ManagedGraphResult)
35+
assert result.content == "hello world"
3536
assert result.metrics.success is True
3637

3738

@@ -172,7 +173,8 @@ async def test_create_agent_graph_run_produces_result(ldai_client: LDAIClient):
172173

173174
assert managed is not None
174175
result = await managed.run("find restaurants")
175-
assert result.output == "final answer"
176+
assert isinstance(result, ManagedGraphResult)
177+
assert result.content == "final answer"
176178
assert result.metrics.success is True
177179

178180

0 commit comments

Comments
 (0)