Skip to content

Commit 2ba2e9f

Browse files
authored
chore: Addressing blitz feedback (#115)
1 parent 53fd95e commit 2ba2e9f

18 files changed

Lines changed: 1343 additions & 377 deletions

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@ dependencies = [
2424
"langchain>=1.0.0",
2525
]
2626

27+
[project.optional-dependencies]
28+
graph = ["langgraph>=1.0.0"]
29+
2730
[project.urls]
2831
Homepage = "https://docs.launchdarkly.com/sdk/ai/python"
2932
Repository = "https://github.com/launchdarkly/python-server-sdk-ai"
@@ -36,6 +39,7 @@ dev = [
3639
"mypy==1.18.2",
3740
"pycodestyle>=2.11.0",
3841
"isort>=5.12.0",
42+
"langgraph>=1.0.0",
3943
]
4044

4145
[build-system]

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

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,22 @@
1-
"""LangChain agent runner for LaunchDarkly AI SDK."""
2-
31
from typing import Any
42

53
from ldai import log
64
from ldai.providers import AgentResult, AgentRunner
75
from ldai.providers.types import LDAIMetrics
86

9-
from ldai_langchain.langchain_helper import sum_token_usage_from_messages
7+
from ldai_langchain.langchain_helper import (
8+
extract_last_message_content,
9+
sum_token_usage_from_messages,
10+
)
1011

1112

1213
class LangChainAgentRunner(AgentRunner):
1314
"""
15+
CAUTION:
16+
This feature is experimental and should NOT be considered ready for production use.
17+
It may change or be removed without notice and is not subject to backwards
18+
compatibility guarantees.
19+
1420
AgentRunner implementation for LangChain.
1521
1622
Wraps a compiled LangChain agent graph (from ``langchain.agents.create_agent``)
@@ -37,11 +43,7 @@ async def run(self, input: Any) -> AgentResult:
3743
"messages": [{"role": "user", "content": str(input)}]
3844
})
3945
messages = result.get("messages", [])
40-
output = ""
41-
if messages:
42-
last = messages[-1]
43-
if hasattr(last, 'content') and isinstance(last.content, str):
44-
output = last.content
46+
output = extract_last_message_content(messages)
4547
return AgentResult(
4648
output=output,
4749
raw=result,

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

Lines changed: 38 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from typing import Any, Dict, List, Optional, Union
22

33
from langchain_core.language_models.chat_models import BaseChatModel
4-
from langchain_core.messages import AIMessage, BaseMessage, HumanMessage, SystemMessage
4+
from langchain_core.messages import AIMessage, HumanMessage, SystemMessage
55
from ldai import LDMessage, log
66
from ldai.models import AIConfigKind
77
from ldai.providers import ToolRegistry
@@ -51,18 +51,12 @@ def convert_messages_to_langchain(
5151
return result
5252

5353

54-
def create_langchain_model(ai_config: AIConfigKind, tool_registry: Optional[ToolRegistry] = None) -> BaseChatModel:
54+
def create_langchain_model(ai_config: AIConfigKind) -> BaseChatModel:
5555
"""
5656
Create a LangChain BaseChatModel from a LaunchDarkly AI configuration.
5757
58-
If the config includes tool definitions and a tool_registry is provided, tools found
59-
in the registry are bound to the model. Tools not found in the registry are skipped
60-
with a warning. Built-in provider tools (e.g. code_interpreter) are not supported
61-
via LangChain's bind_tools abstraction and are skipped with a warning.
62-
6358
:param ai_config: The LaunchDarkly AI configuration
64-
:param tool_registry: Optional registry mapping tool names to callable implementations
65-
:return: A configured LangChain BaseChatModel, with tools bound if applicable
59+
:return: A configured LangChain BaseChatModel
6660
"""
6761
from langchain.chat_models import init_chat_model
6862

@@ -73,112 +67,51 @@ def create_langchain_model(ai_config: AIConfigKind, tool_registry: Optional[Tool
7367
model_name = model_dict.get('name', '')
7468
provider = provider_dict.get('name', '')
7569
parameters = dict(model_dict.get('parameters') or {})
76-
tool_definitions = parameters.pop('tools', []) or []
70+
parameters.pop('tools', None)
7771
mapped_provider = map_provider(provider)
7872

7973
# Bedrock requires the foundation provider (e.g. Bedrock:Anthropic) passed in
8074
# parameters separately from model_provider, which is used for LangChain routing.
8175
if mapped_provider == 'bedrock_converse' and 'provider' not in parameters:
8276
parameters['provider'] = provider.removeprefix('bedrock:')
8377

84-
model = init_chat_model(
78+
return init_chat_model(
8579
model_name,
8680
model_provider=mapped_provider,
8781
**parameters,
8882
)
8983

90-
if tool_definitions and tool_registry is not None:
91-
bindable = _resolve_tools_for_langchain(tool_definitions, tool_registry)
92-
if bindable:
93-
model = model.bind_tools(bindable)
94-
95-
return model
96-
97-
98-
def _iter_valid_tools(
99-
tool_definitions: List[Dict[str, Any]],
100-
tool_registry: ToolRegistry,
101-
) -> List[tuple]:
102-
"""
103-
Filter LD tool definitions against a registry, returning (name, td) pairs for each
104-
valid function tool that has a callable implementation. Built-in provider tools and
105-
tools missing from the registry are skipped with a warning.
106-
"""
107-
valid = []
108-
for td in tool_definitions:
109-
if not isinstance(td, dict):
110-
continue
111-
112-
tool_type = td.get('type')
113-
if tool_type and tool_type != 'function':
114-
log.warning(
115-
f"Built-in tool '{tool_type}' is not reliably supported via LangChain and will be skipped. "
116-
"Use a provider-specific runner to use built-in provider tools."
117-
)
118-
continue
119-
120-
name = td.get('name')
121-
if not name:
122-
continue
123-
124-
if name not in tool_registry:
125-
log.warning(f"Tool '{name}' is defined in the AI config but was not found in the tool registry; skipping.")
126-
continue
127-
128-
valid.append((name, td))
129-
130-
return valid
131-
13284

133-
def _resolve_tools_for_langchain(
134-
tool_definitions: List[Dict[str, Any]],
135-
tool_registry: ToolRegistry,
136-
) -> List[Dict[str, Any]]:
137-
"""
138-
Match LD tool definitions against a registry, returning function-calling tool dicts
139-
for tools that have a callable implementation. Built-in provider tools and tools
140-
missing from the registry are skipped with a warning.
141-
"""
142-
return [
143-
{
144-
'type': 'function',
145-
'function': {
146-
'name': name,
147-
'description': td.get('description', ''),
148-
'parameters': td.get('parameters', {'type': 'object', 'properties': {}}),
149-
},
150-
}
151-
for name, td in _iter_valid_tools(tool_definitions, tool_registry)
152-
]
153-
154-
155-
def build_structured_tools(ai_config: AIConfigKind, tool_registry: ToolRegistry) -> List[Any]:
85+
def build_tools(ai_config: AIConfigKind, tool_registry: ToolRegistry) -> List[Any]:
15686
"""
157-
Build a list of LangChain StructuredTool instances from LD tool definitions and a registry.
87+
Return callables from the registry for each tool defined in the AI config.
15888
159-
Tools found in the registry are wrapped as StructuredTool with the name and description
160-
from the LD config. Built-in provider tools and tools missing from the registry are
161-
skipped with a warning.
89+
Tools not found in the registry are skipped with a warning. The returned
90+
callables can be passed directly to bind_tools or langchain.agents.create_agent.
91+
Functions should have type-annotated parameters so LangChain can infer the schema.
16292
16393
:param ai_config: The LaunchDarkly AI configuration
16494
:param tool_registry: Registry mapping tool names to callable implementations
165-
:return: List of StructuredTool instances ready to pass to langchain.agents.create_agent
95+
:return: List of callables ready to pass to bind_tools or create_agent
16696
"""
167-
from langchain_core.tools import StructuredTool
168-
16997
config_dict = ai_config.to_dict()
17098
model_dict = config_dict.get('model') or {}
17199
parameters = dict(model_dict.get('parameters') or {})
172100
tool_definitions = parameters.pop('tools', []) or []
173101

174-
return [
175-
StructuredTool.from_function(
176-
func=tool_registry[name],
177-
name=name,
178-
description=td.get('description', ''),
179-
)
180-
for name, td in _iter_valid_tools(tool_definitions, tool_registry)
181-
]
102+
tools = []
103+
for td in tool_definitions:
104+
if not isinstance(td, dict):
105+
continue
106+
name = td.get('name')
107+
if not name:
108+
continue
109+
fn = tool_registry.get(name)
110+
if fn is None:
111+
log.warning(f"Tool '{name}' is defined in the AI config but was not found in the tool registry; skipping.")
112+
continue
113+
tools.append(fn)
114+
return tools
182115

183116

184117
def get_ai_usage_from_response(response: Any) -> Optional[TokenUsage]:
@@ -234,6 +167,20 @@ def get_tool_calls_from_response(response: Any) -> List[str]:
234167
return names
235168

236169

170+
def extract_last_message_content(messages: List[Any]) -> str:
171+
"""
172+
Extract the string content of the last message in a list.
173+
174+
:param messages: List of LangChain message objects
175+
:return: String content of the last message, or empty string if none or content is not a str
176+
"""
177+
if messages:
178+
last = messages[-1]
179+
if hasattr(last, 'content') and isinstance(last.content, str):
180+
return last.content
181+
return ''
182+
183+
237184
def sum_token_usage_from_messages(messages: List[Any]) -> Optional[TokenUsage]:
238185
"""
239186
Sum token usage across LangChain messages using get_ai_usage_from_response per message.

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

Lines changed: 31 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
from ldai_langchain.langchain_agent_runner import LangChainAgentRunner
77
from ldai_langchain.langchain_helper import (
8-
build_structured_tools,
8+
build_tools,
99
create_langchain_model,
1010
)
1111
from ldai_langchain.langchain_model_runner import LangChainModelRunner
@@ -14,8 +14,38 @@
1414
class LangChainRunnerFactory(AIProvider):
1515
"""LangChain ``AIProvider`` implementation for the LaunchDarkly AI SDK."""
1616

17+
def create_agent(self, config: Any, tools: Optional[ToolRegistry] = None) -> LangChainAgentRunner:
18+
"""
19+
CAUTION:
20+
This feature is experimental and should NOT be considered ready for production use.
21+
It may change or be removed without notice and is not subject to backwards
22+
compatibility guarantees.
23+
24+
Create a configured LangChainAgentRunner for the given AI agent config.
25+
26+
:param config: The LaunchDarkly AI agent configuration
27+
:param tools: ToolRegistry mapping tool names to callables
28+
:return: LangChainAgentRunner ready to run the agent
29+
"""
30+
from langchain.agents import create_agent as lc_create_agent
31+
instructions = (config.instructions or '') if hasattr(config, 'instructions') else ''
32+
llm = create_langchain_model(config)
33+
lc_tools = build_tools(config, tools or {})
34+
35+
agent = lc_create_agent(
36+
llm,
37+
tools=lc_tools or None,
38+
system_prompt=instructions or None,
39+
)
40+
return LangChainAgentRunner(agent)
41+
1742
def create_agent_graph(self, graph_def: Any, tools: ToolRegistry) -> Any:
1843
"""
44+
CAUTION:
45+
This feature is experimental and should NOT be considered ready for production use.
46+
It may change or be removed without notice and is not subject to backwards
47+
compatibility guarantees.
48+
1949
Create a configured LangGraphAgentGraphRunner for the given graph definition.
2050
2151
:param graph_def: The AgentGraphDefinition to execute
@@ -36,23 +66,3 @@ def create_model(self, config: AIConfigKind) -> LangChainModelRunner:
3666
"""
3767
llm = create_langchain_model(config)
3868
return LangChainModelRunner(llm)
39-
40-
def create_agent(self, config: Any, tools: Optional[ToolRegistry] = None) -> LangChainAgentRunner:
41-
"""
42-
Create a configured LangChainAgentRunner for the given AI agent config.
43-
44-
:param config: The LaunchDarkly AI agent configuration
45-
:param tools: ToolRegistry mapping tool names to callables
46-
:return: LangChainAgentRunner ready to run the agent
47-
"""
48-
from langchain.agents import create_agent as lc_create_agent
49-
instructions = (config.instructions or '') if hasattr(config, 'instructions') else ''
50-
llm = create_langchain_model(config)
51-
lc_tools = build_structured_tools(config, tools or {})
52-
53-
agent = lc_create_agent(
54-
llm,
55-
tools=lc_tools or None,
56-
system_prompt=instructions or None,
57-
)
58-
return LangChainAgentRunner(agent)

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

Lines changed: 9 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
"""LangGraph agent graph runner for LaunchDarkly AI SDK."""
2-
31
import operator
42
import time
53
from typing import Annotated, Any, List
@@ -10,7 +8,9 @@
108
from ldai.providers.types import LDAIMetrics
119

1210
from ldai_langchain.langchain_helper import (
11+
build_tools,
1312
create_langchain_model,
13+
extract_last_message_content,
1414
get_ai_metrics_from_response,
1515
get_ai_usage_from_response,
1616
get_tool_calls_from_response,
@@ -20,6 +20,11 @@
2020

2121
class LangGraphAgentGraphRunner(AgentGraphRunner):
2222
"""
23+
CAUTION:
24+
This feature is experimental and should NOT be considered ready for production use.
25+
It may change or be removed without notice and is not subject to backwards
26+
compatibility guarantees.
27+
2328
AgentGraphRunner implementation for LangGraph.
2429
2530
Compiles and runs the agent graph with LangGraph and automatically records
@@ -73,12 +78,7 @@ def handle_traversal(node: AgentGraphNode, ctx: dict) -> None:
7378
model = None
7479
if node_config.model:
7580
lc_model = create_langchain_model(node_config)
76-
tool_defs = node_config.model.get_parameter('tools') or []
77-
tool_fns = [
78-
tools_ref[t.get('name', '')]
79-
for t in tool_defs
80-
if t.get('name', '') in tools_ref
81-
]
81+
tool_fns = build_tools(node_config, tools_ref)
8282
model = lc_model.bind_tools(tool_fns) if tool_fns else lc_model
8383

8484
def invoke(state: WorkflowState) -> WorkflowState:
@@ -124,12 +124,8 @@ def invoke(state: WorkflowState) -> WorkflowState:
124124
)
125125
duration = (time.perf_counter_ns() - start_ns) // 1_000_000
126126

127-
output = ''
128127
messages = result.get('messages', [])
129-
if messages:
130-
last = messages[-1]
131-
if hasattr(last, 'content'):
132-
output = str(last.content)
128+
output = extract_last_message_content(messages)
133129

134130
if tracker:
135131
tracker.track_path(exec_path)

packages/ai-providers/server-ai-langchain/tests/test_langchain_provider.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -422,7 +422,7 @@ def test_creates_agent_runner_with_instructions_and_tool_definitions(self):
422422

423423
mock_agent = MagicMock()
424424
with patch('ldai_langchain.langchain_runner_factory.create_langchain_model') as mock_create, \
425-
patch('ldai_langchain.langchain_runner_factory.build_structured_tools') as mock_tools, \
425+
patch('ldai_langchain.langchain_runner_factory.build_tools') as mock_tools, \
426426
patch('langchain.agents.create_agent', return_value=mock_agent):
427427
mock_create.return_value = MagicMock()
428428
mock_tools.return_value = [MagicMock()]
@@ -447,7 +447,7 @@ def test_creates_agent_runner_with_no_tools(self):
447447

448448
mock_agent = MagicMock()
449449
with patch('ldai_langchain.langchain_runner_factory.create_langchain_model') as mock_create, \
450-
patch('ldai_langchain.langchain_runner_factory.build_structured_tools', return_value=[]), \
450+
patch('ldai_langchain.langchain_runner_factory.build_tools', return_value=[]), \
451451
patch('langchain.agents.create_agent', return_value=mock_agent):
452452
mock_create.return_value = MagicMock()
453453

0 commit comments

Comments
 (0)