Skip to content

Commit d2d8130

Browse files
Merge branch 'main' into aaronz/SDK-2319/release-please-v5-upgrade
2 parents e3781c0 + 7d6ad23 commit d2d8130

38 files changed

Lines changed: 1337 additions & 903 deletions

.release-please-manifest.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
2-
"packages/sdk/server-ai": "0.18.0",
3-
"packages/ai-providers/server-ai-langchain": "0.5.0",
4-
"packages/ai-providers/server-ai-openai": "0.4.0",
2+
"packages/sdk/server-ai": "0.19.0",
3+
"packages/ai-providers/server-ai-langchain": "0.6.0",
4+
"packages/ai-providers/server-ai-openai": "0.5.0",
55
"packages/optimization": "0.1.0"
66
}

packages/ai-providers/server-ai-langchain/CHANGELOG.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,26 @@
22

33
All notable changes to the LaunchDarkly Python AI LangChain provider package will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org).
44

5+
## [0.6.0](https://github.com/launchdarkly/python-server-sdk-ai/compare/launchdarkly-server-sdk-ai-langchain-0.5.0...launchdarkly-server-sdk-ai-langchain-0.6.0) (2026-05-05)
6+
7+
8+
### ⚠ BREAKING CHANGES
9+
10+
* `LangChainModelRunner.invoke_model()` and `invoke_structured_model()` have been removed. Use the unified `run(input, output_type=...)` method instead, which returns `RunnerResult` in place of `ModelResponse` / `StructuredResponse`.
11+
12+
13+
### Features
14+
15+
* Add judge evaluation support to agent graphs ([#142](https://github.com/launchdarkly/python-server-sdk-ai/issues/142)) ([3d5a6a9](https://github.com/launchdarkly/python-server-sdk-ai/commit/3d5a6a91a87c7475a83a7e440cd4b71337cfd56f))
16+
* Migrate LangGraph runner to AgentGraphRunnerResult; clean up legacy shape detection ([#156](https://github.com/launchdarkly/python-server-sdk-ai/issues/156)) ([efa8e00](https://github.com/launchdarkly/python-server-sdk-ai/commit/efa8e00103d3870d379167769ae38f438b019ec4))
17+
* Support conversation history directly in AI Provider model runners ([#166](https://github.com/launchdarkly/python-server-sdk-ai/issues/166)) ([4bb3e78](https://github.com/launchdarkly/python-server-sdk-ai/commit/4bb3e7813f7c087302ba8446dea6a4a41f012c2e))
18+
* Update LangChain runners to implement Runner protocol returning RunnerResult ([#150](https://github.com/launchdarkly/python-server-sdk-ai/issues/150)) ([62a8e25](https://github.com/launchdarkly/python-server-sdk-ai/commit/62a8e252f4389884fa2f6a90e325db4a8f79376a))
19+
20+
21+
### Bug Fixes
22+
23+
* build judge input as string; strip legacy judge config messages ([#165](https://github.com/launchdarkly/python-server-sdk-ai/issues/165)) ([e6942a6](https://github.com/launchdarkly/python-server-sdk-ai/commit/e6942a6e2d4db17ae1fa6191521f8ac4fb48f30d))
24+
525
## [0.5.0](https://github.com/launchdarkly/python-server-sdk-ai/compare/launchdarkly-server-sdk-ai-langchain-0.4.1...launchdarkly-server-sdk-ai-langchain-0.5.0) (2026-04-21)
626

727

packages/ai-providers/server-ai-langchain/pyproject.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "launchdarkly-server-sdk-ai-langchain"
3-
version = "0.5.0"
3+
version = "0.6.0"
44
description = "LaunchDarkly AI SDK LangChain Provider"
55
authors = [{name = "LaunchDarkly", email = "dev@launchdarkly.com"}]
66
license = {text = "Apache-2.0"}
@@ -20,7 +20,7 @@ classifiers = [
2020
"Topic :: Software Development :: Libraries",
2121
]
2222
dependencies = [
23-
"launchdarkly-server-sdk-ai>=0.18.0",
23+
"launchdarkly-server-sdk-ai>=0.19.0",
2424
"langchain-core>=1.0.0",
2525
"langchain>=1.0.0",
2626
]

packages/ai-providers/server-ai-langchain/src/ldai_langchain/langchain_agent_runner.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ def __init__(self, agent: Any):
3333

3434
async def run(
3535
self,
36-
input: Any,
36+
input: str,
3737
output_type: Optional[Dict[str, Any]] = None,
3838
) -> RunnerResult:
3939
"""
@@ -42,7 +42,7 @@ async def run(
4242
Delegates to the compiled LangChain agent, which handles
4343
the tool-calling loop internally.
4444
45-
:param input: The user prompt or input to the agent
45+
:param input: The user prompt string to the agent
4646
:param output_type: Reserved for future structured output support;
4747
currently ignored.
4848
:return: :class:`RunnerResult` with ``content``, ``raw`` response, and

packages/ai-providers/server-ai-langchain/src/ldai_langchain/langchain_model_runner.py

Lines changed: 23 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1-
from typing import Any, Dict, List, Optional
1+
from typing import Any, Dict, List, Optional, cast
22

3+
from langchain_core.chat_history import InMemoryChatMessageHistory
34
from langchain_core.language_models.chat_models import BaseChatModel
4-
from langchain_core.messages import BaseMessage
5+
from langchain_core.messages import BaseMessage, HumanMessage
56
from ldai import LDMessage, log
67
from ldai.providers.runner import Runner
78
from ldai.providers.types import LDAIMetrics, RunnerResult
@@ -24,8 +25,11 @@ class LangChainModelRunner(Runner):
2425
:meth:`run`.
2526
"""
2627

27-
def __init__(self, llm: BaseChatModel):
28+
def __init__(self, llm: BaseChatModel, config_messages: Optional[List[LDMessage]] = None):
2829
self._llm = llm
30+
self._chat_history = InMemoryChatMessageHistory(
31+
messages=cast(List[BaseMessage], convert_messages_to_langchain(config_messages or []))
32+
)
2933

3034
def get_llm(self) -> BaseChatModel:
3135
"""
@@ -37,41 +41,35 @@ def get_llm(self) -> BaseChatModel:
3741

3842
async def run(
3943
self,
40-
input: Any,
44+
input: str,
4145
output_type: Optional[Dict[str, Any]] = None,
4246
) -> RunnerResult:
4347
"""
4448
Run the LangChain model with the given input.
4549
46-
:param input: A string prompt or a list of :class:`LDMessage` objects
50+
:param input: A string prompt
4751
:param output_type: Optional JSON schema dict requesting structured output.
4852
When provided, ``parsed`` on the returned :class:`RunnerResult` is
4953
populated with the parsed JSON document.
5054
:return: :class:`RunnerResult` containing ``content``, ``metrics``,
5155
``raw`` and (when ``output_type`` is set) ``parsed``.
5256
"""
53-
messages = self._coerce_input(input)
57+
langchain_messages = self._chat_history.messages + [HumanMessage(content=input)]
5458

5559
if output_type is not None:
56-
return await self._run_structured(messages, output_type)
57-
return await self._run_completion(messages)
58-
59-
# convert_messages_to_langchain only accepts List[LDMessage]; _coerce_input
60-
# normalizes a bare string to [LDMessage(role='user', ...)] before that step.
61-
@staticmethod
62-
def _coerce_input(input: Any) -> List[LDMessage]:
63-
if isinstance(input, str):
64-
return [LDMessage(role='user', content=input)]
65-
if isinstance(input, list):
66-
return input
67-
raise TypeError(
68-
f"Unsupported input type for LangChainModelRunner.run: {type(input).__name__}"
69-
)
60+
result = await self._run_structured(langchain_messages, output_type)
61+
else:
62+
result = await self._run_completion(langchain_messages)
63+
64+
if result.metrics.success and result.content:
65+
self._chat_history.add_user_message(input)
66+
self._chat_history.add_ai_message(result.content)
67+
68+
return result
7069

71-
async def _run_completion(self, messages: List[LDMessage]) -> RunnerResult:
70+
async def _run_completion(self, messages: List[BaseMessage]) -> RunnerResult:
7271
try:
73-
langchain_messages = convert_messages_to_langchain(messages)
74-
response: BaseMessage = await self._llm.ainvoke(langchain_messages)
72+
response: BaseMessage = await self._llm.ainvoke(messages)
7573
metrics = get_ai_metrics_from_response(response)
7674

7775
content: str = ''
@@ -98,13 +96,12 @@ async def _run_completion(self, messages: List[LDMessage]) -> RunnerResult:
9896

9997
async def _run_structured(
10098
self,
101-
messages: List[LDMessage],
99+
messages: List[BaseMessage],
102100
output_type: Dict[str, Any],
103101
) -> RunnerResult:
104102
try:
105-
langchain_messages = convert_messages_to_langchain(messages)
106103
structured_llm = self._llm.with_structured_output(output_type, include_raw=True)
107-
response = await structured_llm.ainvoke(langchain_messages)
104+
response = await structured_llm.ainvoke(messages)
108105

109106
if not isinstance(response, dict):
110107
log.warning(f'Structured output did not return a dict. Got: {type(response)}')

packages/ai-providers/server-ai-langchain/src/ldai_langchain/langchain_runner_factory.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,4 +69,5 @@ def create_model(self, config: AIConfigKind) -> LangChainModelRunner:
6969
:return: LangChainModelRunner ready to invoke the model
7070
"""
7171
llm = create_langchain_model(config)
72-
return LangChainModelRunner(llm)
72+
config_messages = list(getattr(config, 'messages', None) or [])
73+
return LangChainModelRunner(llm, config_messages)

packages/ai-providers/server-ai-langchain/src/ldai_langchain/langgraph_agent_graph_runner.py

Lines changed: 29 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,12 @@
11
"""LangGraph agent graph runner for LaunchDarkly AI SDK."""
22

3-
import asyncio
43
import time
5-
from contextvars import ContextVar
64
from typing import Annotated, Any, Dict, List, Set, Tuple
75

86
from ldai import log
97
from ldai.agent_graph import AgentGraphDefinition, AgentGraphNode
10-
from ldai.providers import AgentGraphResult, AgentGraphRunner, ToolRegistry
11-
from ldai.providers.types import LDAIMetrics
8+
from ldai.providers import AgentGraphRunner, ToolRegistry
9+
from ldai.providers.types import AgentGraphRunnerResult, GraphMetrics
1210

1311
from ldai_langchain.langchain_helper import (
1412
build_structured_tools,
@@ -18,9 +16,6 @@
1816
)
1917
from ldai_langchain.langgraph_callback_handler import LDMetricsCallbackHandler
2018

21-
# Per-run eval task accumulator, isolated per concurrent run() call via ContextVar.
22-
_run_eval_tasks: ContextVar[Dict[str, List[asyncio.Task]]] = ContextVar('_run_eval_tasks')
23-
2419

2520
def _make_handoff_tool(child_key: str, description: str) -> Any:
2621
"""
@@ -65,9 +60,10 @@ class LangGraphAgentGraphRunner(AgentGraphRunner):
6560
6661
AgentGraphRunner implementation for LangGraph.
6762
68-
Compiles and runs the agent graph with LangGraph and automatically records
69-
graph- and node-level AI metric data to the LaunchDarkly trackers on the
70-
graph definition and each node.
63+
Compiles and runs the agent graph with LangGraph and collects graph- and
64+
node-level metrics via a LangChain callback handler. Tracking events are
65+
emitted by the managed layer (:class:`~ldai.ManagedAgentGraph`) from the
66+
returned :class:`~ldai.providers.types.AgentGraphRunnerResult`.
7167
7268
Requires ``langgraph`` to be installed.
7369
"""
@@ -181,26 +177,6 @@ async def invoke(state: WorkflowState) -> dict:
181177
if node_instructions:
182178
msgs = [SystemMessage(content=node_instructions)] + msgs
183179
response = await bound_model.ainvoke(msgs)
184-
185-
node_obj = self._graph.get_node(nk)
186-
if node_obj is not None:
187-
input_text = '\r\n'.join(
188-
m.content if isinstance(m.content, str) else str(m.content)
189-
for m in msgs
190-
) if msgs else ''
191-
output_text = (
192-
response.content if hasattr(response, 'content') else str(response)
193-
)
194-
task = node_obj.get_config().evaluator.evaluate(input_text, output_text)
195-
run_tasks = _run_eval_tasks.get(None)
196-
if run_tasks is not None:
197-
run_tasks.setdefault(nk, []).append(task)
198-
else:
199-
log.warning(
200-
f"LangGraphAgentGraphRunner: eval task for node '{nk}' "
201-
"has no run context; judge results will not be tracked"
202-
)
203-
204180
return {'messages': [response]}
205181

206182
invoke.__name__ = nk
@@ -298,20 +274,18 @@ def route(state: WorkflowState) -> str:
298274
compiled = agent_builder.compile()
299275
return compiled, fn_name_to_config_key, node_keys
300276

301-
async def run(self, input: Any) -> AgentGraphResult:
277+
async def run(self, input: Any) -> AgentGraphRunnerResult:
302278
"""
303279
Run the agent graph with the given input.
304280
305281
Builds a LangGraph StateGraph from the AgentGraphDefinition, compiles
306282
it, and invokes it. Uses a LangChain callback handler to collect
307-
per-node metrics, then flushes them to LaunchDarkly trackers.
283+
per-node metrics. Graph-level tracking events are emitted by the
284+
managed layer from the returned GraphMetrics.
308285
309286
:param input: The string prompt to send to the agent graph
310-
:return: AgentGraphResult with the final output and metrics
287+
:return: AgentGraphRunnerResult with the final content and GraphMetrics
311288
"""
312-
pending_eval_tasks: Dict[str, List[asyncio.Task]] = {}
313-
token = _run_eval_tasks.set(pending_eval_tasks)
314-
tracker = self._graph.create_tracker()
315289
start_ns = time.perf_counter_ns()
316290

317291
try:
@@ -325,24 +299,23 @@ async def run(self, input: Any) -> AgentGraphResult:
325299
config={'callbacks': [handler], 'recursion_limit': 25},
326300
)
327301

328-
duration = (time.perf_counter_ns() - start_ns) // 1_000_000
302+
duration_ms = (time.perf_counter_ns() - start_ns) // 1_000_000
329303
messages = result.get('messages', [])
330304
output = extract_last_message_content(messages)
305+
total_usage = sum_token_usage_from_messages(messages)
331306

332-
# Flush per-node metrics to LD trackers; eval results are tracked
333-
# internally and intentionally not exposed on AgentGraphResult here
334-
# — judge dispatch is the managed layer's responsibility.
335-
await handler.flush(self._graph, pending_eval_tasks)
336-
337-
tracker.track_path(handler.path)
338-
tracker.track_duration(duration)
339-
tracker.track_invocation_success()
340-
tracker.track_total_tokens(sum_token_usage_from_messages(messages))
307+
node_metrics = handler.node_metrics
341308

342-
return AgentGraphResult(
343-
output=output,
309+
return AgentGraphRunnerResult(
310+
content=output,
344311
raw=result,
345-
metrics=LDAIMetrics(success=True),
312+
metrics=GraphMetrics(
313+
success=True,
314+
path=handler.path,
315+
duration_ms=duration_ms,
316+
usage=total_usage if (total_usage is not None and total_usage.total > 0) else None,
317+
node_metrics=node_metrics,
318+
),
346319
)
347320

348321
except Exception as exc:
@@ -353,13 +326,12 @@ async def run(self, input: Any) -> AgentGraphResult:
353326
)
354327
else:
355328
log.warning(f'LangGraphAgentGraphRunner run failed: {exc}')
356-
duration = (time.perf_counter_ns() - start_ns) // 1_000_000
357-
tracker.track_duration(duration)
358-
tracker.track_invocation_failure()
359-
return AgentGraphResult(
360-
output='',
329+
duration_ms = (time.perf_counter_ns() - start_ns) // 1_000_000
330+
return AgentGraphRunnerResult(
331+
content='',
361332
raw=None,
362-
metrics=LDAIMetrics(success=False),
333+
metrics=GraphMetrics(
334+
success=False,
335+
duration_ms=duration_ms,
336+
),
363337
)
364-
finally:
365-
_run_eval_tasks.reset(token)

0 commit comments

Comments
 (0)