Skip to content

Commit 80d4e11

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 ca37e74 commit 80d4e11

6 files changed

Lines changed: 139 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,7 +34,11 @@
3434
from ldai.providers import (
3535
AgentGraphResult,
3636
AgentGraphRunner,
37+
AgentGraphRunnerResult,
3738
AgentRunner,
39+
GraphMetrics,
40+
GraphMetricSummary,
41+
ManagedGraphResult,
3842
ManagedResult,
3943
Runner,
4044
RunnerResult,
@@ -49,6 +53,10 @@
4953
'AgentRunner',
5054
'AgentGraphRunner',
5155
'AgentGraphResult',
56+
'AgentGraphRunnerResult',
57+
'GraphMetrics',
58+
'GraphMetricSummary',
59+
'ManagedGraphResult',
5260
'ManagedResult',
5361
'Runner',
5462
'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,8 +6,12 @@
66
from ldai.providers.runner_factory import RunnerFactory
77
from ldai.providers.types import (
88
AgentGraphResult,
9+
AgentGraphRunnerResult,
10+
GraphMetrics,
11+
GraphMetricSummary,
912
JudgeResult,
1013
LDAIMetrics,
14+
ManagedGraphResult,
1115
ManagedResult,
1216
RunnerResult,
1317
ToolRegistry,
@@ -17,9 +21,13 @@
1721
'AIProvider',
1822
'AgentGraphResult',
1923
'AgentGraphRunner',
24+
'AgentGraphRunnerResult',
2025
'AgentRunner',
26+
'GraphMetrics',
27+
'GraphMetricSummary',
2128
'JudgeResult',
2229
'LDAIMetrics',
30+
'ManagedGraphResult',
2331
'ManagedResult',
2432
'ModelRunner',
2533
'Runner',

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

Lines changed: 75 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.tracker import LDAIMetricSummary, TokenUsage
@@ -86,6 +86,80 @@ class ManagedResult:
8686
"""Optional asyncio Task that resolves to the list of :class:`JudgeResult` instances when awaited."""
8787

8888

89+
@dataclass
90+
class GraphMetrics:
91+
"""Contains raw metrics from a single agent graph run."""
92+
93+
success: bool
94+
"""Whether the graph run succeeded."""
95+
96+
path: List[str] = field(default_factory=list)
97+
"""Ordered list of node keys visited during the run."""
98+
99+
duration_ms: Optional[int] = None
100+
"""Wall-clock duration of the graph run in milliseconds."""
101+
102+
usage: Optional[TokenUsage] = None
103+
"""Optional aggregate token usage information across all nodes in the graph run."""
104+
105+
node_metrics: Dict[str, LDAIMetrics] = field(default_factory=dict)
106+
"""Per-node metrics keyed by node key."""
107+
108+
109+
@dataclass
110+
class GraphMetricSummary:
111+
"""Contains a summary of metrics for an agent graph run."""
112+
113+
success: bool
114+
"""Whether the graph run succeeded."""
115+
116+
path: List[str] = field(default_factory=list)
117+
"""Ordered list of node keys visited during the run."""
118+
119+
duration_ms: Optional[int] = None
120+
"""Wall-clock duration of the graph run in milliseconds."""
121+
122+
usage: Optional[TokenUsage] = None
123+
"""Optional aggregate token usage information across all nodes in the graph run."""
124+
125+
node_metrics: Dict[str, LDAIMetrics] = field(default_factory=dict)
126+
"""Per-node metrics keyed by node key."""
127+
128+
resumption_token: Optional[str] = None
129+
"""Optional resumption token from the graph tracker for cross-process resumption."""
130+
131+
132+
@dataclass
133+
class ManagedGraphResult:
134+
"""Contains the result of a managed agent graph run, including metrics and optional judge evaluations."""
135+
136+
content: str
137+
"""The graph's final output content."""
138+
139+
metrics: GraphMetricSummary
140+
"""Aggregated metric summary from the graph tracker for this run."""
141+
142+
raw: Optional[Any] = None
143+
"""Optional provider-native response object for advanced consumers."""
144+
145+
evaluations: Optional[asyncio.Task[List[JudgeResult]]] = None
146+
"""Optional asyncio Task that resolves to the list of :class:`JudgeResult` instances when awaited."""
147+
148+
149+
@dataclass
150+
class AgentGraphRunnerResult:
151+
"""Contains the result of an agent graph runner invocation."""
152+
153+
content: str
154+
"""The graph's final output content."""
155+
156+
metrics: GraphMetrics
157+
"""Metrics from the graph run."""
158+
159+
raw: Optional[Any] = None
160+
"""Optional provider-native response object for advanced consumers."""
161+
162+
89163
@dataclass
90164
class JudgeResult:
91165
"""Contains the result of a single judge evaluation."""

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)