22
33from langchain_core .language_models .chat_models import BaseChatModel
44from langchain_core .messages import AIMessage , BaseMessage , HumanMessage , SystemMessage
5- from ldai import LDMessage
5+ from ldai import LDMessage , log
66from ldai .models import AIConfigKind
7+ from ldai .providers import ToolRegistry
78from ldai .providers .types import LDAIMetrics
89from 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,113 @@ 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 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+
132+
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 ]:
156+ """
157+ Build a list of LangChain StructuredTool instances from LD tool definitions and a registry.
158+
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.
162+
163+ :param ai_config: The LaunchDarkly AI configuration
164+ :param tool_registry: Registry mapping tool names to callable implementations
165+ :return: List of StructuredTool instances ready to pass to langchain.agents.create_agent
166+ """
167+ from langchain_core .tools import StructuredTool
168+
169+ config_dict = ai_config .to_dict ()
170+ model_dict = config_dict .get ('model' ) or {}
171+ parameters = dict (model_dict .get ('parameters' ) or {})
172+ tool_definitions = parameters .pop ('tools' , []) or []
173+
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+ ]
182+
82183
83184def get_ai_usage_from_response (response : Any ) -> Optional [TokenUsage ]:
84185 """
@@ -88,11 +189,11 @@ def get_ai_usage_from_response(response: Any) -> Optional[TokenUsage]:
88189 :return: TokenUsage or None if unavailable
89190 """
90191 if hasattr (response , 'usage_metadata' ) and response .usage_metadata :
91- return TokenUsage (
92- total = response .usage_metadata .get ('total_tokens ' , 0 ),
93- input = response .usage_metadata .get ('input_tokens ' , 0 ),
94- output = response . usage_metadata . get ( 'output_tokens' , 0 ),
95- )
192+ total = response . usage_metadata . get ( 'total_tokens' , 0 )
193+ inp = response .usage_metadata .get ('input_tokens ' , 0 )
194+ out = response .usage_metadata .get ('output_tokens ' , 0 )
195+ if total or inp or out :
196+ return TokenUsage ( total = total , input = inp , output = out )
96197 if hasattr (response , 'response_metadata' ) and response .response_metadata :
97198 token_usage = (
98199 response .response_metadata .get ('tokenUsage' )
0 commit comments