Skip to content

Commit 05e1f4c

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 9c0003c commit 05e1f4c

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,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: 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.models import LDMessage
@@ -114,6 +114,80 @@ class StructuredResponse:
114114
metrics: LDAIMetrics
115115

116116

117+
@dataclass
118+
class GraphMetrics:
119+
"""Contains raw metrics from a single agent graph run."""
120+
121+
success: bool
122+
"""Whether the graph run succeeded."""
123+
124+
path: List[str] = field(default_factory=list)
125+
"""Ordered list of node keys visited during the run."""
126+
127+
duration_ms: Optional[int] = None
128+
"""Wall-clock duration of the graph run in milliseconds."""
129+
130+
usage: Optional[TokenUsage] = None
131+
"""Optional aggregate token usage information across all nodes in the graph run."""
132+
133+
node_metrics: Dict[str, LDAIMetrics] = field(default_factory=dict)
134+
"""Per-node metrics keyed by node key."""
135+
136+
137+
@dataclass
138+
class GraphMetricSummary:
139+
"""Contains a summary of metrics for an agent graph run."""
140+
141+
success: bool
142+
"""Whether the graph run succeeded."""
143+
144+
path: List[str] = field(default_factory=list)
145+
"""Ordered list of node keys visited during the run."""
146+
147+
duration_ms: Optional[int] = None
148+
"""Wall-clock duration of the graph run in milliseconds."""
149+
150+
usage: Optional[TokenUsage] = None
151+
"""Optional aggregate token usage information across all nodes in the graph run."""
152+
153+
node_metrics: Dict[str, LDAIMetrics] = field(default_factory=dict)
154+
"""Per-node metrics keyed by node key."""
155+
156+
resumption_token: Optional[str] = None
157+
"""Optional resumption token from the graph tracker for cross-process resumption."""
158+
159+
160+
@dataclass
161+
class ManagedGraphResult:
162+
"""Contains the result of a managed agent graph run, including metrics and optional judge evaluations."""
163+
164+
content: str
165+
"""The graph's final output content."""
166+
167+
metrics: GraphMetricSummary
168+
"""Aggregated metric summary from the graph tracker for this run."""
169+
170+
raw: Optional[Any] = None
171+
"""Optional provider-native response object for advanced consumers."""
172+
173+
evaluations: Optional[asyncio.Task[List[JudgeResult]]] = None
174+
"""Optional asyncio Task that resolves to the list of :class:`JudgeResult` instances when awaited."""
175+
176+
177+
@dataclass
178+
class AgentGraphRunnerResult:
179+
"""Contains the result of an agent graph runner invocation."""
180+
181+
content: str
182+
"""The graph's final output content."""
183+
184+
metrics: GraphMetrics
185+
"""Metrics from the graph run."""
186+
187+
raw: Optional[Any] = None
188+
"""Optional provider-native response object for advanced consumers."""
189+
190+
117191
@dataclass
118192
class JudgeResult:
119193
"""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)