1+ import inspect
12from typing import Any , Dict , List , Optional , Union
23
34from langchain_core .language_models .chat_models import BaseChatModel
@@ -156,9 +157,10 @@ def build_structured_tools(ai_config: AIConfigKind, tool_registry: ToolRegistry)
156157 """
157158 Build a list of LangChain StructuredTool instances from LD tool definitions and a registry.
158159
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.
160+ Tools found in the registry are wrapped as StructuredTool using the LD config key as the
161+ tool name so the model's tool calls match ToolNode lookup. Async callables use ``coroutine=``
162+ so LangGraph invokes them correctly. Built-in provider tools and tools missing from the
163+ registry are skipped with a warning.
162164
163165 :param ai_config: The LaunchDarkly AI configuration
164166 :param tool_registry: Registry mapping tool names to callable implementations
@@ -171,14 +173,17 @@ def build_structured_tools(ai_config: AIConfigKind, tool_registry: ToolRegistry)
171173 parameters = dict (model_dict .get ('parameters' ) or {})
172174 tool_definitions = parameters .pop ('tools' , []) or []
173175
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- ]
176+ tools = []
177+ for name , td in _iter_valid_tools (tool_definitions , tool_registry ):
178+ fn = tool_registry [name ]
179+ raw_desc = td .get ('description' ) if isinstance (td .get ('description' ), str ) else ''
180+ description = raw_desc .strip () or (getattr (fn , '__doc__' , None ) or '' ).strip () or f'Tool { name } '
181+ if inspect .iscoroutinefunction (fn ):
182+ tool = StructuredTool .from_function (coroutine = fn , name = name , description = description )
183+ else :
184+ tool = StructuredTool .from_function (fn , name = name , description = description )
185+ tools .append (tool )
186+ return tools
182187
183188
184189def get_ai_usage_from_response (response : Any ) -> Optional [TokenUsage ]:
@@ -234,6 +239,20 @@ def get_tool_calls_from_response(response: Any) -> List[str]:
234239 return names
235240
236241
242+ def extract_last_message_content (messages : List [Any ]) -> str :
243+ """
244+ Extract the string content of the last message in a list.
245+
246+ :param messages: List of LangChain message objects
247+ :return: String content of the last message, or empty string if none or content is not a str
248+ """
249+ if messages :
250+ last = messages [- 1 ]
251+ if hasattr (last , 'content' ) and isinstance (last .content , str ):
252+ return last .content
253+ return ''
254+
255+
237256def sum_token_usage_from_messages (messages : List [Any ]) -> Optional [TokenUsage ]:
238257 """
239258 Sum token usage across LangChain messages using get_ai_usage_from_response per message.
0 commit comments