codekingpro/portable-devtools
114k
1"""Wrapper around Minimax chat models."""2 3import json4import logging5from contextlib import asynccontextmanager, contextmanager6from operator import itemgetter7from typing import (8 Any,9 AsyncIterator,10 Callable,11 Dict,12 Iterator,13 List,14 Optional,15 Sequence,16 Type,17 Union,18)19 20from langchain_core.callbacks import (21 AsyncCallbackManagerForLLMRun,22 CallbackManagerForLLMRun,23)24from langchain_core.language_models import LanguageModelInput25from langchain_core.language_models.chat_models import (26 BaseChatModel,27 agenerate_from_stream,28 generate_from_stream,29)30from langchain_core.messages import (31 AIMessage,32 AIMessageChunk,33 BaseMessage,34 BaseMessageChunk,35 ChatMessage,36 ChatMessageChunk,37 HumanMessage,38 SystemMessage,39 ToolMessage,40)41from langchain_core.output_parsers.base import OutputParserLike42from langchain_core.output_parsers.openai_tools import (43 JsonOutputKeyToolsParser,44 PydanticToolsParser,45)46from langchain_core.outputs import ChatGeneration, ChatGenerationChunk, ChatResult47from langchain_core.runnables import Runnable, RunnableMap, RunnablePassthrough48from langchain_core.tools import BaseTool49from langchain_core.utils import convert_to_secret_str, get_from_dict_or_env50from langchain_core.utils.function_calling import convert_to_openai_tool51from langchain_core.utils.pydantic import get_fields52from pydantic import (53 BaseModel,54 ConfigDict,55 Field,56 SecretStr,57 model_validator,58)59 60logger = logging.getLogger(__name__)61 62 63@contextmanager64def connect_httpx_sse(client: Any, method: str, url: str, **kwargs: Any) -> Iterator:65 """Context manager for connecting to an SSE stream.66 67 Args:68 client: The httpx client.69 method: The HTTP method.70 url: The URL to connect to.71 kwargs: Additional keyword arguments to pass to the client.72 73 Yields:74 An EventSource object.75 """76 from httpx_sse import EventSource77 78 with client.stream(method, url, **kwargs) as response:79 yield EventSource(response)80 81 82@asynccontextmanager83async def aconnect_httpx_sse(84 client: Any, method: str, url: str, **kwargs: Any85) -> AsyncIterator:86 """Async context manager for connecting to an SSE stream.87 88 Args:89 client: The httpx client.90 method: The HTTP method.91 url: The URL to connect to.92 kwargs: Additional keyword arguments to pass to the client.93 94 Yields:95 An EventSource object.96 """97 from httpx_sse import EventSource98 99 async with client.stream(method, url, **kwargs) as response:100 yield EventSource(response)101 102 103def _convert_message_to_dict(message: BaseMessage) -> Dict[str, Any]:104 """Convert a LangChain messages to Dict."""105 message_dict: Dict[str, Any]106 if isinstance(message, HumanMessage):107 message_dict = {"role": "user", "content": message.content}108 elif isinstance(message, AIMessage):109 message_dict = {110 "role": "assistant",111 "content": message.content,112 "tool_calls": message.additional_kwargs.get("tool_calls"),113 }114 elif isinstance(message, SystemMessage):115 message_dict = {"role": "system", "content": message.content}116 elif isinstance(message, ToolMessage):117 message_dict = {118 "role": "tool",119 "content": message.content,120 "tool_call_id": message.tool_call_id,121 "name": message.name or message.additional_kwargs.get("name"),122 }123 else:124 raise TypeError(f"Got unknown type '{message.__class__.__name__}'.")125 return message_dict126 127 128def _convert_dict_to_message(dct: Dict[str, Any]) -> BaseMessage:129 """Convert a dict to LangChain message."""130 role = dct.get("role")131 content = dct.get("content", "")132 if role == "assistant":133 additional_kwargs = {}134 tool_calls = dct.get("tool_calls", None)135 if tool_calls is not None:136 additional_kwargs["tool_calls"] = tool_calls137 return AIMessage(content=content, additional_kwargs=additional_kwargs)138 return ChatMessage(role=role, content=content) # type: ignore[arg-type]139 140 141def _convert_delta_to_message_chunk(142 dct: Dict[str, Any], default_class: Type[BaseMessageChunk]143) -> BaseMessageChunk:144 role = dct.get("role")145 content = dct.get("content", "")146 additional_kwargs = {}147 tool_calls = dct.get("tool_call", None)148 if tool_calls is not None:149 additional_kwargs["tool_calls"] = tool_calls150 151 if role == "assistant" or default_class == AIMessageChunk:152 return AIMessageChunk(content=content, additional_kwargs=additional_kwargs)153 if role or default_class == ChatMessageChunk:154 return ChatMessageChunk(content=content, role=role) # type: ignore[arg-type]155 return default_class(content=content) # type: ignore[call-arg]156 157 158class MiniMaxChat(BaseChatModel):159 """MiniMax chat model integration.160 161 Setup:162 To use, you should have the environment variable``MINIMAX_API_KEY`` set with163 your API KEY.164 165 .. code-block:: bash166 167 export MINIMAX_API_KEY="your-api-key"168 169 Key init args — completion params:170 model: Optional[str]171 Name of MiniMax model to use.172 max_tokens: Optional[int]173 Max number of tokens to generate.174 temperature: Optional[float]175 Sampling temperature.176 top_p: Optional[float]177 Total probability mass of tokens to consider at each step.178 streaming: Optional[bool]179 Whether to stream the results or not.180 181 Key init args — client params:182 api_key: Optional[str]183 MiniMax API key. If not passed in will be read from env var MINIMAX_API_KEY.184 base_url: Optional[str]185 Base URL for API requests.186 187 See full list of supported init args and their descriptions in the params section.188 189 Instantiate:190 .. code-block:: python191 192 from langchain_community.chat_models import MiniMaxChat193 194 chat = MiniMaxChat(195 api_key=api_key,196 model='abab6.5-chat',197 # temperature=...,198 # other params...199 )200 201 Invoke:202 .. code-block:: python203 204 messages = [205 ("system", "你是一名专业的翻译家,可以将用户的中文翻译为英文。"),206 ("human", "我喜欢编程。"),207 ]208 chat.invoke(messages)209 210 .. code-block:: python211 212 AIMessage(213 content='I enjoy programming.',214 response_metadata={215 'token_usage': {'total_tokens': 48},216 'model_name': 'abab6.5-chat',217 'finish_reason': 'stop'218 },219 id='run-42d62ba6-5dc1-4e16-98dc-f72708a4162d-0'220 )221 222 Stream:223 .. code-block:: python224 225 for chunk in chat.stream(messages):226 print(chunk)227 228 .. code-block:: python229 230 content='I' id='run-a5837c45-4aaa-4f64-9ab4-2679bbd55522'231 content=' enjoy programming.' response_metadata={'finish_reason': 'stop'} id='run-a5837c45-4aaa-4f64-9ab4-2679bbd55522'232 233 .. code-block:: python234 235 stream = chat.stream(messages)236 full = next(stream)237 for chunk in stream:238 full += chunk239 full240 241 .. code-block:: python242 243 AIMessageChunk(244 content='I enjoy programming.',245 response_metadata={'finish_reason': 'stop'},246 id='run-01aed0a0-61c4-4709-be22-c6d8b17155d6'247 )248 249 Async:250 .. code-block:: python251 252 await chat.ainvoke(messages)253 254 # stream255 # async for chunk in chat.astream(messages):256 # print(chunk)257 258 # batch259 # await chat.abatch([messages])260 261 .. code-block:: python262 263 AIMessage(264 content='I enjoy programming.',265 response_metadata={266 'token_usage': {'total_tokens': 48},267 'model_name': 'abab6.5-chat',268 'finish_reason': 'stop'269 },270 id='run-c263b6f1-1736-4ece-a895-055c26b3436f-0'271 )272 273 Tool calling:274 .. code-block:: python275 276 from pydantic import BaseModel, Field277 278 279 class GetWeather(BaseModel):280 '''Get the current weather in a given location'''281 282 location: str = Field(283 ..., description="The city and state, e.g. San Francisco, CA"284 )285 286 287 class GetPopulation(BaseModel):288 '''Get the current population in a given location'''289 290 location: str = Field(291 ..., description="The city and state, e.g. San Francisco, CA"292 )293 294 chat_with_tools = chat.bind_tools([GetWeather, GetPopulation])295 ai_msg = chat_with_tools.invoke(296 "Which city is hotter today and which is bigger: LA or NY?"297 )298 ai_msg.tool_calls299 300 .. code-block:: python301 302 [303 {304 'name': 'GetWeather',305 'args': {'location': 'LA'},306 'id': 'call_function_2140449382',307 'type': 'tool_call'308 }309 ]310 311 Structured output:312 .. code-block:: python313 314 from typing import Optional315 316 from pydantic import BaseModel, Field317 318 319 class Joke(BaseModel):320 '''Joke to tell user.'''321 setup: str = Field(description="The setup of the joke")322 punchline: str = Field(description="The punchline to the joke")323 rating: Optional[int] = Field(description="How funny the joke is, from 1 to 10")324 325 326 structured_chat = chat.with_structured_output(Joke)327 structured_chat.invoke("Tell me a joke about cats")328 329 .. code-block:: python330 331 Joke(332 setup='Why do cats have nine lives?',333 punchline='Because they are so cute and cuddly!',334 rating=None335 )336 337 Response metadata338 .. code-block:: python339 340 ai_msg = chat.invoke(messages)341 ai_msg.response_metadata342 343 .. code-block:: python344 345 {'token_usage': {'total_tokens': 48},346 'model_name': 'abab6.5-chat',347 'finish_reason': 'stop'}348 349 """ # noqa: E501350 351 @property352 def _identifying_params(self) -> Dict[str, Any]:353 """Get the identifying parameters."""354 return {**{"model": self.model}, **self._default_params}355 356 @property357 def _llm_type(self) -> str:358 """Return type of llm."""359 return "minimax"360 361 @property362 def _default_params(self) -> Dict[str, Any]:363 """Get the default parameters for calling OpenAI API."""364 return {365 "model": self.model,366 "max_tokens": self.max_tokens,367 "temperature": self.temperature,368 "top_p": self.top_p,369 **self.model_kwargs,370 }371 372 _client: Any = None373 model: str = "abab6.5s-chat"374 """Model name to use."""375 max_tokens: int = 256376 """Denotes the number of tokens to predict per generation."""377 temperature: float = 0.7378 """A non-negative float that tunes the degree of randomness in generation."""379 top_p: float = 0.95380 """Total probability mass of tokens to consider at each step."""381 model_kwargs: Dict[str, Any] = Field(default_factory=dict)382 """Holds any model parameters valid for `create` call not explicitly specified."""383 minimax_api_host: str = Field(384 default="https://api.minimaxi.chat/v1/text/chatcompletion_v2", alias="base_url"385 )386 minimax_group_id: Optional[str] = Field(default=None, alias="group_id")387 """[DEPRECATED, keeping it for for backward compatibility] Group Id"""388 minimax_api_key: SecretStr = Field(alias="api_key")389 """Minimax API Key"""390 streaming: bool = False391 """Whether to stream the results or not."""392 393 model_config = ConfigDict(394 populate_by_name=True,395 )396 397 @model_validator(mode="before")398 @classmethod399 def validate_environment(cls, values: Dict) -> Any:400 """Validate that api key and python package exists in environment."""401 values["minimax_api_key"] = convert_to_secret_str(402 get_from_dict_or_env(403 values,404 ["minimax_api_key", "api_key"],405 "MINIMAX_API_KEY",406 )407 )408 409 default_values = {410 name: field.default411 for name, field in get_fields(cls).items()412 if field.default is not None413 }414 default_values.update(values)415 416 # Get custom api url from environment.417 values["minimax_api_host"] = get_from_dict_or_env(418 values,419 ["minimax_api_host", "base_url"],420 "MINIMAX_API_HOST",421 default_values["minimax_api_host"],422 )423 return values424 425 def _create_chat_result(self, response: Union[dict, BaseModel]) -> ChatResult:426 generations = []427 if not isinstance(response, dict):428 response = response.dict()429 for res in response["choices"]:430 message = _convert_dict_to_message(res["message"])431 generation_info = dict(finish_reason=res.get("finish_reason"))432 generations.append(433 ChatGeneration(message=message, generation_info=generation_info)434 )435 token_usage = response.get("usage", {})436 llm_output = {437 "token_usage": token_usage,438 "model_name": self.model,439 }440 return ChatResult(generations=generations, llm_output=llm_output)441 442 def _create_payload_parameters(443 self, messages: List[BaseMessage], is_stream: bool = False, **kwargs: Any444 ) -> Dict[str, Any]:445 """Create API request body parameters."""446 message_dicts = [_convert_message_to_dict(m) for m in messages]447 payload = self._default_params448 payload["messages"] = message_dicts449 450 self._reformat_function_parameters(kwargs.get("tools", {}))451 payload.update(**kwargs)452 453 if is_stream:454 payload["stream"] = True455 456 return payload457 458 @staticmethod459 def _reformat_function_parameters(tools_arg: Dict[Any, Any]) -> None:460 """Reformat the function parameters to strings."""461 for tool_arg in tools_arg:462 if tool_arg["type"] == "function" and not isinstance(463 tool_arg["function"]["parameters"], str464 ):465 tool_arg["function"]["parameters"] = json.dumps(466 tool_arg["function"]["parameters"]467 )468 469 def _generate(470 self,471 messages: List[BaseMessage],472 stop: Optional[List[str]] = None,473 run_manager: Optional[CallbackManagerForLLMRun] = None,474 stream: Optional[bool] = None,475 **kwargs: Any,476 ) -> ChatResult:477 """Generate next turn in the conversation.478 Args:479 messages: The history of the conversation as a list of messages. Code chat480 does not support context.481 stop: The list of stop words (optional).482 run_manager: The CallbackManager for LLM run, it's not used at the moment.483 stream: Whether to stream the results or not.484 485 Returns:486 The ChatResult that contains outputs generated by the model.487 488 Raises:489 ValueError: if the last message in the list is not from human.490 """491 if not messages:492 raise ValueError(493 "You should provide at least one message to start the chat!"494 )495 is_stream = stream if stream is not None else self.streaming496 if is_stream:497 stream_iter = self._stream(498 messages, stop=stop, run_manager=run_manager, **kwargs499 )500 return generate_from_stream(stream_iter)501 payload = self._create_payload_parameters(messages, **kwargs)502 api_key = ""503 if self.minimax_api_key is not None:504 api_key = self.minimax_api_key.get_secret_value()505 headers = {506 "Authorization": f"Bearer {api_key}",507 "Content-Type": "application/json",508 }509 import httpx510 511 with httpx.Client(headers=headers, timeout=60) as client:512 response = client.post(self.minimax_api_host, json=payload)513 response.raise_for_status()514 final_response = response.json()515 if (516 "base_resp" in final_response517 and "status_msg" in final_response["base_resp"]518 and final_response["base_resp"]["status_msg"] == "invalid api key"519 ):520 raise Exception("Invalid API Key Provided")521 return self._create_chat_result(response.json())522 523 def _stream(524 self,525 messages: List[BaseMessage],526 stop: Optional[List[str]] = None,527 run_manager: Optional[CallbackManagerForLLMRun] = None,528 **kwargs: Any,529 ) -> Iterator[ChatGenerationChunk]:530 """Stream the chat response in chunks."""531 payload = self._create_payload_parameters(messages, is_stream=True, **kwargs)532 api_key = ""533 if self.minimax_api_key is not None:534 api_key = self.minimax_api_key.get_secret_value()535 headers = {536 "Authorization": f"Bearer {api_key}",537 "Content-Type": "application/json",538 }539 import httpx540 541 with httpx.Client(headers=headers, timeout=60) as client:542 with connect_httpx_sse(543 client, "POST", self.minimax_api_host, json=payload544 ) as event_source:545 for sse in event_source.iter_sse():546 chunk = json.loads(sse.data)547 if len(chunk["choices"]) == 0:548 continue549 choice = chunk["choices"][0]550 chunk = _convert_delta_to_message_chunk(551 choice["delta"], AIMessageChunk552 )553 finish_reason = choice.get("finish_reason", None)554 555 generation_info = (556 {"finish_reason": finish_reason}557 if finish_reason is not None558 else None559 )560 chunk = ChatGenerationChunk(561 message=chunk, generation_info=generation_info562 )563 if run_manager:564 run_manager.on_llm_new_token(chunk.text, chunk=chunk)565 yield chunk566 567 if finish_reason is not None:568 break569 570 async def _agenerate(571 self,572 messages: List[BaseMessage],573 stop: Optional[List[str]] = None,574 run_manager: Optional[AsyncCallbackManagerForLLMRun] = None,575 stream: Optional[bool] = None,576 **kwargs: Any,577 ) -> ChatResult:578 if not messages:579 raise ValueError(580 "You should provide at least one message to start the chat!"581 )582 is_stream = stream if stream is not None else self.streaming583 if is_stream:584 stream_iter = self._astream(585 messages, stop=stop, run_manager=run_manager, **kwargs586 )587 return await agenerate_from_stream(stream_iter)588 payload = self._create_payload_parameters(messages, **kwargs)589 api_key = ""590 if self.minimax_api_key is not None:591 api_key = self.minimax_api_key.get_secret_value()592 headers = {593 "Authorization": f"Bearer {api_key}",594 "Content-Type": "application/json",595 }596 import httpx597 598 async with httpx.AsyncClient(headers=headers, timeout=60) as client:599 response = await client.post(self.minimax_api_host, json=payload)600 response.raise_for_status()601 return self._create_chat_result(response.json())602 603 async def _astream(604 self,605 messages: List[BaseMessage],606 stop: Optional[List[str]] = None,607 run_manager: Optional[AsyncCallbackManagerForLLMRun] = None,608 **kwargs: Any,609 ) -> AsyncIterator[ChatGenerationChunk]:610 payload = self._create_payload_parameters(messages, is_stream=True, **kwargs)611 api_key = ""612 if self.minimax_api_key is not None:613 api_key = self.minimax_api_key.get_secret_value()614 headers = {615 "Authorization": f"Bearer {api_key}",616 "Content-Type": "application/json",617 }618 import httpx619 620 async with httpx.AsyncClient(headers=headers, timeout=60) as client:621 async with aconnect_httpx_sse(622 client, "POST", self.minimax_api_host, json=payload623 ) as event_source:624 async for sse in event_source.aiter_sse():625 chunk = json.loads(sse.data)626 if len(chunk["choices"]) == 0:627 continue628 choice = chunk["choices"][0]629 chunk = _convert_delta_to_message_chunk(630 choice["delta"], AIMessageChunk631 )632 finish_reason = choice.get("finish_reason", None)633 634 generation_info = (635 {"finish_reason": finish_reason}636 if finish_reason is not None637 else None638 )639 chunk = ChatGenerationChunk(640 message=chunk, generation_info=generation_info641 )642 if run_manager:643 await run_manager.on_llm_new_token(chunk.text, chunk=chunk)644 yield chunk645 646 if finish_reason is not None:647 break648 649 def bind_tools(650 self,651 tools: Sequence[Union[Dict[str, Any], Type[BaseModel], Callable, BaseTool]],652 **kwargs: Any,653 ) -> Runnable[LanguageModelInput, AIMessage]:654 """Bind tool-like objects to this chat model.655 656 Args:657 tools: A list of tool definitions to bind to this chat model.658 Can be a dictionary, pydantic model, callable, or BaseTool. Pydantic659 models, callables, and BaseTools will be automatically converted to660 their schema dictionary representation.661 **kwargs: Any additional parameters to pass to the662 :class: `~langchain.runnable.Runnable` constructor.663 """664 665 formatted_tools = [convert_to_openai_tool(tool) for tool in tools]666 return super().bind(tools=formatted_tools, **kwargs)667 668 def with_structured_output(669 self,670 schema: Union[Dict, Type[BaseModel]],671 *,672 include_raw: bool = False,673 **kwargs: Any,674 ) -> Runnable[LanguageModelInput, Union[Dict, BaseModel]]:675 """Model wrapper that returns outputs formatted to match the given schema.676 677 Args:678 schema: The output schema as a dict or a Pydantic class. If a Pydantic class679 then the model output will be an object of that class. If a dict then680 the model output will be a dict. With a Pydantic class the returned681 attributes will be validated, whereas with a dict they will not be. If682 `method` is "function_calling" and `schema` is a dict, then the dict683 must match the OpenAI function-calling spec.684 include_raw: If False then only the parsed structured output is returned. If685 an error occurs during model output parsing it will be raised. If True686 then both the raw model response (a BaseMessage) and the parsed model687 response will be returned. If an error occurs during output parsing it688 will be caught and returned as well. The final output is always a dict689 with keys "raw", "parsed", and "parsing_error".690 691 Returns:692 A Runnable that takes any ChatModel input and returns as output:693 694 If include_raw is True then a dict with keys:695 raw: BaseMessage696 parsed: Optional[_DictOrPydantic]697 parsing_error: Optional[BaseException]698 699 If include_raw is False then just _DictOrPydantic is returned,700 where _DictOrPydantic depends on the schema:701 702 If schema is a Pydantic class then _DictOrPydantic is the Pydantic703 class.704 705 If schema is a dict then _DictOrPydantic is a dict.706 707 Example: Function-calling, Pydantic schema (method="function_calling", include_raw=False):708 .. code-block:: python709 710 from langchain_community.chat_models import MiniMaxChat711 from pydantic import BaseModel712 713 class AnswerWithJustification(BaseModel):714 '''An answer to the user question along with justification for the answer.'''715 answer: str716 justification: str717 718 llm = MiniMaxChat()719 structured_llm = llm.with_structured_output(AnswerWithJustification)720 721 structured_llm.invoke("What weighs more a pound of bricks or a pound of feathers")722 723 # -> AnswerWithJustification(724 # answer='A pound of bricks and a pound of feathers weigh the same.',725 # justification='The weight of the feathers is much less dense than the weight of the bricks, but since both weigh one pound, they weigh the same.'726 # )727 728 Example: Function-calling, Pydantic schema (method="function_calling", include_raw=True):729 .. code-block:: python730 731 from langchain_community.chat_models import MiniMaxChat732 from pydantic import BaseModel733 734 class AnswerWithJustification(BaseModel):735 '''An answer to the user question along with justification for the answer.'''736 answer: str737 justification: str738 739 llm = MiniMaxChat()740 structured_llm = llm.with_structured_output(AnswerWithJustification, include_raw=True)741 742 structured_llm.invoke("What weighs more a pound of bricks or a pound of feathers")743 744 # -> {745 # 'raw': AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_function_8953642285', 'type': 'function', 'function': {'name': 'AnswerWithJustification', 'arguments': '{"answer": "A pound of bricks and a pound of feathers weigh the same.", "justification": "The weight of the feathers is much less dense than the weight of the bricks, but since both weigh one pound, they weigh the same."}'}}]}, response_metadata={'token_usage': {'total_tokens': 257}, 'model_name': 'abab6.5-chat', 'finish_reason': 'tool_calls'}, id='run-d897e037-2796-49f5-847e-f9f69dd390db-0', tool_calls=[{'name': 'AnswerWithJustification', 'args': {'answer': 'A pound of bricks and a pound of feathers weigh the same.', 'justification': 'The weight of the feathers is much less dense than the weight of the bricks, but since both weigh one pound, they weigh the same.'}, 'id': 'call_function_8953642285', 'type': 'tool_call'}]),746 # 'parsed': AnswerWithJustification(answer='A pound of bricks and a pound of feathers weigh the same.', justification='The weight of the feathers is much less dense than the weight of the bricks, but since both weigh one pound, they weigh the same.'),747 # 'parsing_error': None748 # }749 750 Example: Function-calling, dict schema (method="function_calling", include_raw=False):751 .. code-block:: python752 753 from langchain_community.chat_models import MiniMaxChat754 from pydantic import BaseModel755 from langchain_core.utils.function_calling import convert_to_openai_tool756 757 class AnswerWithJustification(BaseModel):758 '''An answer to the user question along with justification for the answer.'''759 answer: str760 justification: str761 762 dict_schema = convert_to_openai_tool(AnswerWithJustification)763 llm = MiniMaxChat()764 structured_llm = llm.with_structured_output(dict_schema)765 766 structured_llm.invoke("What weighs more a pound of bricks or a pound of feathers")767 768 # -> {769 # 'answer': 'A pound of bricks and a pound of feathers both weigh the same, which is a pound.',770 # 'justification': 'The difference is that bricks are much denser than feathers, so a pound of bricks will take up much less space than a pound of feathers.'771 # }772 """ # noqa: E501773 if kwargs:774 raise ValueError(f"Received unsupported arguments {kwargs}")775 is_pydantic_schema = isinstance(schema, type) and issubclass(schema, BaseModel)776 llm = self.bind_tools([schema])777 if is_pydantic_schema:778 output_parser: OutputParserLike = PydanticToolsParser(779 tools=[schema], # type: ignore[list-item]780 first_tool_only=True,781 )782 else:783 key_name = convert_to_openai_tool(schema)["function"]["name"]784 output_parser = JsonOutputKeyToolsParser(785 key_name=key_name, first_tool_only=True786 )787 788 if include_raw:789 parser_assign = RunnablePassthrough.assign(790 parsed=itemgetter("raw") | output_parser, parsing_error=lambda _: None791 )792 parser_none = RunnablePassthrough.assign(parsed=lambda _: None)793 parser_with_fallback = parser_assign.with_fallbacks(794 [parser_none], exception_key="parsing_error"795 )796 return RunnableMap(raw=llm) | parser_with_fallback797 else:798 return llm | output_parser799 