Ensure functions have docstrings for documentation
def parse_tool_call(
1"""Parse tools for OpenAI tools output."""23import copy4import json5import logging6from json import JSONDecodeError7from typing import Annotated, Any89from pydantic import SkipValidation, ValidationError1011from langchain_core.exceptions import OutputParserException12from langchain_core.messages import AIMessage, InvalidToolCall13from langchain_core.messages.tool import invalid_tool_call14from langchain_core.messages.tool import tool_call as create_tool_call15from langchain_core.output_parsers.transform import BaseCumulativeTransformOutputParser16from langchain_core.outputs import ChatGeneration, Generation17from langchain_core.utils.json import parse_partial_json18from langchain_core.utils.pydantic import (19 TypeBaseModel,20 is_pydantic_v1_subclass,21 is_pydantic_v2_subclass,22)2324logger = logging.getLogger(__name__)252627def parse_tool_call(28 raw_tool_call: dict[str, Any],29 *,30 partial: bool = False,31 strict: bool = False,32 return_id: bool = True,33) -> dict[str, Any] | None:34 """Parse a single tool call.3536 Args:37 raw_tool_call: The raw tool call to parse.38 partial: Whether to parse partial JSON.39 strict: Whether to allow non-JSON-compliant strings.40 return_id: Whether to return the tool call id.4142 Returns:43 The parsed tool call.4445 Raises:46 OutputParserException: If the tool call is not valid JSON.47 """48 if "function" not in raw_tool_call:49 return None5051 arguments = raw_tool_call["function"]["arguments"]5253 if partial:54 try:55 function_args = parse_partial_json(arguments, strict=strict)56 except (JSONDecodeError, TypeError): # None args raise TypeError57 return None58 # Handle None or empty string arguments for parameter-less tools59 elif not arguments:60 function_args = {}61 else:62 try:63 function_args = json.loads(arguments, strict=strict)64 except JSONDecodeError as e:65 msg = (66 f"Function {raw_tool_call['function']['name']} arguments:\n\n"67 f"{arguments}\n\nare not valid JSON. "68 f"Received JSONDecodeError {e}"69 )70 raise OutputParserException(msg) from e71 parsed = {72 "name": raw_tool_call["function"]["name"] or "",73 "args": function_args or {},74 }75 if return_id:76 parsed["id"] = raw_tool_call.get("id")77 parsed = create_tool_call(**parsed) # type: ignore[assignment,arg-type]78 return parsed798081def make_invalid_tool_call(82 raw_tool_call: dict[str, Any],83 error_msg: str | None,84) -> InvalidToolCall:85 """Create an `InvalidToolCall` from a raw tool call.8687 Args:88 raw_tool_call: The raw tool call.89 error_msg: The error message.9091 Returns:92 An `InvalidToolCall` instance with the error message.93 """94 return invalid_tool_call(95 name=raw_tool_call["function"]["name"],96 args=raw_tool_call["function"]["arguments"],97 id=raw_tool_call.get("id"),98 error=error_msg,99 )100101102def parse_tool_calls(103 raw_tool_calls: list[dict],104 *,105 partial: bool = False,106 strict: bool = False,107 return_id: bool = True,108) -> list[dict[str, Any]]:109 """Parse a list of tool calls.110111 Args:112 raw_tool_calls: The raw tool calls to parse.113 partial: Whether to parse partial JSON.114 strict: Whether to allow non-JSON-compliant strings.115 return_id: Whether to return the tool call id.116117 Returns:118 The parsed tool calls.119120 Raises:121 OutputParserException: If any of the tool calls are not valid JSON.122 """123 final_tools: list[dict[str, Any]] = []124 exceptions = []125 for tool_call in raw_tool_calls:126 try:127 parsed = parse_tool_call(128 tool_call, partial=partial, strict=strict, return_id=return_id129 )130 if parsed:131 final_tools.append(parsed)132 except OutputParserException as e:133 exceptions.append(str(e))134 continue135 if exceptions:136 raise OutputParserException("\n\n".join(exceptions))137 return final_tools138139140class JsonOutputToolsParser(BaseCumulativeTransformOutputParser[Any]):141 """Parse tools from OpenAI response."""142143 strict: bool = False144 """Whether to allow non-JSON-compliant strings.145146 See: https://docs.python.org/3/library/json.html#encoders-and-decoders147148 Useful when the parsed output may include unicode characters or new lines.149 """150151 return_id: bool = False152 """Whether to return the tool call id."""153154 first_tool_only: bool = False155 """Whether to return only the first tool call.156157 If `False`, the result will be a list of tool calls, or an empty list if no tool158 calls are found.159160 If `True`, and multiple tool calls are found, only the first one will be returned,161 and the other tool calls will be ignored.162163 If no tool calls are found, `None` will be returned.164 """165166 def parse_result(self, result: list[Generation], *, partial: bool = False) -> Any:167 """Parse the result of an LLM call to a list of tool calls.168169 Args:170 result: The result of the LLM call.171 partial: Whether to parse partial JSON.172173 If `True`, the output will be a JSON object containing174 all the keys that have been returned so far.175176 If `False`, the output will be the full JSON object.177178 Returns:179 The parsed tool calls.180181 Raises:182 OutputParserException: If the output is not valid JSON.183 """184 generation = result[0]185 if not isinstance(generation, ChatGeneration):186 msg = "This output parser can only be used with a chat generation."187 raise OutputParserException(msg)188 message = generation.message189 if isinstance(message, AIMessage) and message.tool_calls:190 tool_calls = [dict(tc) for tc in message.tool_calls]191 for tool_call in tool_calls:192 if not self.return_id:193 _ = tool_call.pop("id")194 else:195 try:196 raw_tool_calls = copy.deepcopy(message.additional_kwargs["tool_calls"])197 except KeyError:198 return []199 tool_calls = parse_tool_calls(200 raw_tool_calls,201 partial=partial,202 strict=self.strict,203 return_id=self.return_id,204 )205 # for backwards compatibility206 for tc in tool_calls:207 tc["type"] = tc.pop("name")208209 if self.first_tool_only:210 return tool_calls[0] if tool_calls else None211 return tool_calls212213 def parse(self, text: str) -> Any:214 """Parse the output of an LLM call to a list of tool calls.215216 Args:217 text: The output of the LLM call.218219 Returns:220 The parsed tool calls.221 """222 raise NotImplementedError223224225class JsonOutputKeyToolsParser(JsonOutputToolsParser):226 """Parse tools from OpenAI response."""227228 key_name: str229 """The type of tools to return."""230231 def parse_result(self, result: list[Generation], *, partial: bool = False) -> Any:232 """Parse the result of an LLM call to a list of tool calls.233234 Args:235 result: The result of the LLM call.236 partial: Whether to parse partial JSON.237 If `True`, the output will be a JSON object containing238 all the keys that have been returned so far.239 If `False`, the output will be the full JSON object.240241 Raises:242 OutputParserException: If the generation is not a chat generation.243244 Returns:245 The parsed tool calls.246 """247 generation = result[0]248 if not isinstance(generation, ChatGeneration):249 msg = "This output parser can only be used with a chat generation."250 raise OutputParserException(msg)251 message = generation.message252 if isinstance(message, AIMessage) and message.tool_calls:253 parsed_tool_calls = [dict(tc) for tc in message.tool_calls]254 for tool_call in parsed_tool_calls:255 if not self.return_id:256 _ = tool_call.pop("id")257 else:258 try:259 # This exists purely for backward compatibility / cached messages260 # All new messages should use `message.tool_calls`261 raw_tool_calls = copy.deepcopy(message.additional_kwargs["tool_calls"])262 except KeyError:263 if self.first_tool_only:264 return None265 return []266 parsed_tool_calls = parse_tool_calls(267 raw_tool_calls,268 partial=partial,269 strict=self.strict,270 return_id=self.return_id,271 )272 # For backwards compatibility273 for tc in parsed_tool_calls:274 tc["type"] = tc.pop("name")275 if self.first_tool_only:276 parsed_result = list(277 filter(lambda x: x["type"] == self.key_name, parsed_tool_calls)278 )279 single_result = (280 parsed_result[0]281 if parsed_result and parsed_result[0]["type"] == self.key_name282 else None283 )284 if self.return_id:285 return single_result286 if single_result:287 return single_result["args"]288 return None289 return (290 [res for res in parsed_tool_calls if res["type"] == self.key_name]291 if self.return_id292 else [293 res["args"] for res in parsed_tool_calls if res["type"] == self.key_name294 ]295 )296297298# Common cause of ValidationError is truncated output due to max_tokens.299_MAX_TOKENS_ERROR = (300 "Output parser received a `max_tokens` stop reason. "301 "The output is likely incomplete—please increase `max_tokens` "302 "or shorten your prompt."303)304305306class PydanticToolsParser(JsonOutputToolsParser):307 """Parse tools from OpenAI response."""308309 tools: Annotated[list[TypeBaseModel], SkipValidation()]310 """The tools to parse."""311312 # TODO: Support more granular streaming of objects.313 # Currently only streams once all Pydantic object fields are present.314 def parse_result(self, result: list[Generation], *, partial: bool = False) -> Any:315 """Parse the result of an LLM call to a list of Pydantic objects.316317 Args:318 result: The result of the LLM call.319 partial: Whether to parse partial JSON.320321 If `True`, the output will be a JSON object containing all the keys that322 have been returned so far.323324 If `False`, the output will be the full JSON object.325326 Returns:327 The parsed Pydantic objects.328329 Raises:330 ValueError: If the tool call arguments are not a dict.331 ValidationError: If the tool call arguments do not conform to the Pydantic332 model.333 """334 json_results = super().parse_result(result, partial=partial)335 if not json_results:336 return None if self.first_tool_only else []337338 json_results = [json_results] if self.first_tool_only else json_results339 name_dict_v2: dict[str, TypeBaseModel] = {340 tool.model_config.get("title") or tool.__name__: tool341 for tool in self.tools342 if is_pydantic_v2_subclass(tool)343 }344 name_dict_v1: dict[str, TypeBaseModel] = {345 tool.__name__: tool for tool in self.tools if is_pydantic_v1_subclass(tool)346 }347 name_dict: dict[str, TypeBaseModel] = {**name_dict_v2, **name_dict_v1}348 pydantic_objects = []349 for res in json_results:350 if not isinstance(res["args"], dict):351 if partial:352 continue353 msg = (354 f"Tool arguments must be specified as a dict, received: "355 f"{res['args']}"356 )357 raise ValueError(msg)358359 try:360 tool = name_dict[res["type"]]361 except KeyError as e:362 available = ", ".join(name_dict.keys()) or "<no_tools>"363 msg = (364 f"Unknown tool type: {res['type']!r}. Available tools: {available}"365 )366 raise OutputParserException(msg) from e367368 try:369 pydantic_objects.append(tool(**res["args"]))370 except (ValidationError, ValueError):371 if partial:372 continue373 has_max_tokens_stop_reason = any(374 generation.message.response_metadata.get("stop_reason")375 == "max_tokens"376 for generation in result377 if isinstance(generation, ChatGeneration)378 )379 if has_max_tokens_stop_reason:380 logger.exception(_MAX_TOKENS_ERROR)381 raise382 if self.first_tool_only:383 return pydantic_objects[0] if pydantic_objects else None384 return pydantic_objects
Same data, no extra tab — call code_get_file + code_get_findings over MCP from Claude/Cursor/Copilot.