Skip to content

Commit b1712dc

Browse files
committed
simplifying tool configuration
1 parent 9523fa7 commit b1712dc

6 files changed

Lines changed: 76 additions & 50 deletions

File tree

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

Lines changed: 3 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"""LangChain agent runner for LaunchDarkly AI SDK."""
22

3-
from typing import Any, Dict, List
3+
from typing import Any, List
44

55
from langchain_core.messages import (
66
AIMessage,
@@ -21,19 +21,18 @@ class LangChainAgentRunner(AgentRunner):
2121
AgentRunner implementation for LangChain.
2222
2323
Executes a single-agent loop using a LangChain BaseChatModel with tool calling.
24+
The model is expected to have tools already bound to it.
2425
Returned by LangChainRunnerFactory.create_agent(config, tools).
2526
"""
2627

2728
def __init__(
2829
self,
2930
llm: Any,
3031
instructions: str,
31-
tool_definitions: List[Dict[str, Any]],
3232
tools: ToolRegistry,
3333
):
3434
self._llm = llm
3535
self._instructions = instructions
36-
self._tool_definitions = tool_definitions
3736
self._tools = tools
3837

3938
async def run(self, input: Any) -> AgentResult:
@@ -51,14 +50,11 @@ async def run(self, input: Any) -> AgentResult:
5150
messages.append(SystemMessage(content=self._instructions))
5251
messages.append(HumanMessage(content=str(input)))
5352

54-
openai_tools = self._build_openai_tools()
55-
model = self._llm.bind_tools(openai_tools) if openai_tools else self._llm
56-
5753
raw_response = None
5854

5955
try:
6056
while True:
61-
response: AIMessage = await model.ainvoke(messages)
57+
response: AIMessage = await self._llm.ainvoke(messages)
6258
raw_response = response
6359
messages.append(response)
6460

@@ -103,25 +99,6 @@ async def run(self, input: Any) -> AgentResult:
10399
metrics=LDAIMetrics(success=False, usage=None),
104100
)
105101

106-
def _build_openai_tools(self) -> List[Dict[str, Any]]:
107-
"""Convert LD tool definitions to OpenAI function-calling format for bind_tools."""
108-
tools = []
109-
for td in self._tool_definitions:
110-
if not isinstance(td, dict):
111-
continue
112-
if "type" in td:
113-
tools.append(td)
114-
elif "name" in td:
115-
tools.append({
116-
"type": "function",
117-
"function": {
118-
"name": td["name"],
119-
"description": td.get("description", ""),
120-
"parameters": td.get("parameters", {"type": "object", "properties": {}}),
121-
},
122-
})
123-
return tools
124-
125102
def get_llm(self) -> Any:
126103
"""Return the underlying LangChain LLM."""
127104
return self._llm

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

Lines changed: 62 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@
22

33
from langchain_core.language_models.chat_models import BaseChatModel
44
from langchain_core.messages import AIMessage, BaseMessage, HumanMessage, SystemMessage
5-
from ldai import LDMessage
5+
from ldai import LDMessage, log
66
from ldai.models import AIConfigKind
7+
from ldai.providers import ToolRegistry
78
from ldai.providers.types import LDAIMetrics
89
from ldai.tracker import TokenUsage
910

@@ -50,12 +51,18 @@ def convert_messages_to_langchain(
5051
return result
5152

5253

53-
def create_langchain_model(ai_config: AIConfigKind) -> BaseChatModel:
54+
def create_langchain_model(ai_config: AIConfigKind, tool_registry: Optional[ToolRegistry] = None) -> BaseChatModel:
5455
"""
5556
Create a LangChain BaseChatModel from a LaunchDarkly AI configuration.
5657
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+
5763
:param ai_config: The LaunchDarkly AI configuration
58-
:return: A configured LangChain BaseChatModel
64+
:param tool_registry: Optional registry mapping tool names to callable implementations
65+
:return: A configured LangChain BaseChatModel, with tools bound if applicable
5966
"""
6067
from langchain.chat_models import init_chat_model
6168

@@ -66,19 +73,70 @@ def create_langchain_model(ai_config: AIConfigKind) -> BaseChatModel:
6673
model_name = model_dict.get('name', '')
6774
provider = provider_dict.get('name', '')
6875
parameters = dict(model_dict.get('parameters') or {})
76+
tool_definitions = parameters.pop('tools', []) or []
6977
mapped_provider = map_provider(provider)
7078

7179
# Bedrock requires the foundation provider (e.g. Bedrock:Anthropic) passed in
7280
# parameters separately from model_provider, which is used for LangChain routing.
7381
if mapped_provider == 'bedrock_converse' and 'provider' not in parameters:
7482
parameters['provider'] = provider.removeprefix('bedrock:')
7583

76-
return init_chat_model(
84+
model = init_chat_model(
7785
model_name,
7886
model_provider=mapped_provider,
7987
**parameters,
8088
)
8189

90+
if tool_definitions:
91+
bindable = _resolve_tools_for_langchain(tool_definitions, tool_registry or {})
92+
if bindable:
93+
model = model.bind_tools(bindable)
94+
95+
return model
96+
97+
98+
def _resolve_tools_for_langchain(
99+
tool_definitions: List[Dict[str, Any]],
100+
tool_registry: ToolRegistry,
101+
) -> List[Dict[str, Any]]:
102+
"""
103+
Match LD tool definitions against a registry, returning function-calling tool dicts
104+
for tools that have a callable implementation. Built-in provider tools and tools
105+
missing from the registry are skipped with a warning.
106+
"""
107+
bindable = []
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's "
116+
"bind_tools abstraction and will be skipped. Use a provider-specific runner "
117+
"to use built-in provider tools."
118+
)
119+
continue
120+
121+
name = td.get('name')
122+
if not name:
123+
continue
124+
125+
if name not in tool_registry:
126+
log.warning(f"Tool '{name}' is defined in the AI config but was not found in the tool registry; skipping.")
127+
continue
128+
129+
bindable.append({
130+
'type': 'function',
131+
'function': {
132+
'name': name,
133+
'description': td.get('description', ''),
134+
'parameters': td.get('parameters', {'type': 'object', 'properties': {}}),
135+
},
136+
})
137+
138+
return bindable
139+
82140

83141
def get_ai_usage_from_response(response: Any) -> Optional[TokenUsage]:
84142
"""

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

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from typing import TYPE_CHECKING, Any
1+
from typing import TYPE_CHECKING, Any, Optional
22

33
from ldai.models import AIConfigKind
44
from ldai.providers import AIProvider, ToolRegistry
@@ -36,7 +36,7 @@ def create_model(self, config: AIConfigKind) -> LangChainModelRunner:
3636
llm = create_langchain_model(config)
3737
return LangChainModelRunner(llm)
3838

39-
def create_agent(self, config: Any, tools: Any) -> 'LangChainAgentRunner':
39+
def create_agent(self, config: Any, tools: Optional[ToolRegistry] = None) -> 'LangChainAgentRunner':
4040
"""
4141
Create a configured LangChainAgentRunner for the given AI agent config.
4242
@@ -46,11 +46,6 @@ def create_agent(self, config: Any, tools: Any) -> 'LangChainAgentRunner':
4646
"""
4747
from ldai_langchain.langchain_agent_runner import LangChainAgentRunner
4848

49-
config_dict = config.to_dict()
50-
model_dict = config_dict.get('model') or {}
51-
parameters = dict(model_dict.get('parameters') or {})
52-
tool_definitions = parameters.pop('tools', []) or []
53-
instructions = config.instructions or '' if hasattr(config, 'instructions') else ''
54-
55-
llm = create_langchain_model(config)
56-
return LangChainAgentRunner(llm, instructions, tool_definitions, tools or {})
49+
instructions = (config.instructions or '') if hasattr(config, 'instructions') else ''
50+
llm = create_langchain_model(config, tool_registry=tools or {})
51+
return LangChainAgentRunner(llm, instructions, tools or {})

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

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -429,7 +429,6 @@ def test_creates_agent_runner_with_instructions_and_tool_definitions(self):
429429

430430
assert isinstance(result, LangChainAgentRunner)
431431
assert result._instructions == "You are a helpful assistant."
432-
assert len(result._tool_definitions) == 1
433432

434433
def test_creates_agent_runner_with_no_tools(self):
435434
"""Should create LangChainAgentRunner with no tool definitions."""
@@ -450,7 +449,7 @@ def test_creates_agent_runner_with_no_tools(self):
450449
result = factory.create_agent(mock_ai_config, {})
451450

452451
assert isinstance(result, LangChainAgentRunner)
453-
assert result._tool_definitions == []
452+
assert result._tools == {}
454453

455454

456455
class TestLangChainAgentRunner:
@@ -464,10 +463,9 @@ async def test_runs_agent_and_returns_result_with_no_tool_calls(self):
464463

465464
mock_llm = MagicMock()
466465
mock_response = AIMessage(content="The answer is 42.")
467-
mock_llm.bind_tools = MagicMock(return_value=mock_llm)
468466
mock_llm.ainvoke = AsyncMock(return_value=mock_response)
469467

470-
runner = LangChainAgentRunner(mock_llm, "You are helpful.", [], {})
468+
runner = LangChainAgentRunner(mock_llm, "You are helpful.", {})
471469
result = await runner.run("What is the answer?")
472470

473471
assert result.output == "The answer is 42."
@@ -489,13 +487,11 @@ async def test_executes_tool_calls_and_returns_final_response(self):
489487
second_response = AIMessage(content="It is sunny in Paris.")
490488

491489
mock_llm = MagicMock()
492-
mock_llm.bind_tools = MagicMock(return_value=mock_llm)
493490
mock_llm.ainvoke = AsyncMock(side_effect=[first_response, second_response])
494491

495492
weather_fn = MagicMock(return_value="Sunny, 25°C")
496493
runner = LangChainAgentRunner(
497494
mock_llm, "You are helpful.",
498-
[{'name': 'get-weather', 'description': 'Get weather', 'parameters': {}}],
499495
{'get-weather': weather_fn},
500496
)
501497
result = await runner.run("What is the weather in Paris?")
@@ -510,10 +506,9 @@ async def test_returns_failure_when_exception_thrown(self):
510506
from ldai_langchain import LangChainAgentRunner
511507

512508
mock_llm = MagicMock()
513-
mock_llm.bind_tools = MagicMock(return_value=mock_llm)
514509
mock_llm.ainvoke = AsyncMock(side_effect=Exception("LLM Error"))
515510

516-
runner = LangChainAgentRunner(mock_llm, "", [], {})
511+
runner = LangChainAgentRunner(mock_llm, "", {})
517512
result = await runner.run("Hello")
518513

519514
assert result.output == ""

packages/ai-providers/server-ai-openai/src/ldai_openai/openai_runner_factory.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ def create_agent_graph(self, graph_def: Any, tools: ToolRegistry) -> Any:
5050
from ldai_openai.openai_agent_graph_runner import OpenAIAgentGraphRunner
5151
return OpenAIAgentGraphRunner(graph_def, tools)
5252

53+
5354
def create_agent(self, config: Any, tools: Optional[ToolRegistry] = None) -> 'OpenAIAgentRunner':
5455
"""
5556
Create a configured OpenAIAgentRunner for the given AI agent config.

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
from ldai import log
55
from ldai.models import LDMessage
6-
from ldai.providers.types import ModelResponse, StructuredResponse
6+
from ldai.providers.types import ModelResponse, StructuredResponse, ToolRegistry
77

88

99
class AIProvider(ABC):
@@ -73,7 +73,7 @@ def create_model(self, config: Any) -> Optional[Any]:
7373
log.warning('create_model not implemented by this provider')
7474
return None
7575

76-
def create_agent(self, config: Any, tools: Any) -> Optional[Any]:
76+
def create_agent(self, config: Any, tools: Optional[ToolRegistry] = None) -> Optional[Any]:
7777
"""
7878
Create a configured agent executor for the given AI config and tool registry.
7979

0 commit comments

Comments
 (0)