Overuse may indicate design issues; consider polymorphism
return isinstance(obj, type) and is_basemodel_subclass(obj)
1"""Wrapper around Perplexity APIs."""23from __future__ import annotations45import logging6from collections.abc import AsyncIterator, Iterator, Mapping7from operator import itemgetter8from typing import Any, Literal, TypeAlias, cast910from langchain_core.callbacks import (11 AsyncCallbackManagerForLLMRun,12 CallbackManagerForLLMRun,13)14from langchain_core.language_models import (15 LanguageModelInput,16 ModelProfile,17 ModelProfileRegistry,18)19from langchain_core.language_models.chat_models import (20 BaseChatModel,21 agenerate_from_stream,22 generate_from_stream,23)24from langchain_core.messages import (25 AIMessage,26 AIMessageChunk,27 BaseMessage,28 BaseMessageChunk,29 ChatMessage,30 ChatMessageChunk,31 FunctionMessageChunk,32 HumanMessage,33 HumanMessageChunk,34 SystemMessage,35 SystemMessageChunk,36 ToolMessageChunk,37)38from langchain_core.messages.ai import (39 OutputTokenDetails,40 UsageMetadata,41 subtract_usage,42)43from langchain_core.outputs import ChatGeneration, ChatGenerationChunk, ChatResult44from langchain_core.runnables import Runnable, RunnableMap, RunnablePassthrough45from langchain_core.utils import get_pydantic_field_names, secret_from_env46from langchain_core.utils.function_calling import convert_to_json_schema47from langchain_core.utils.pydantic import is_basemodel_subclass48from perplexity import AsyncPerplexity, Perplexity49from pydantic import BaseModel, ConfigDict, Field, SecretStr, model_validator50from typing_extensions import Self5152from langchain_perplexity.data._profiles import _PROFILES53from langchain_perplexity.output_parsers import (54 ReasoningJsonOutputParser,55 ReasoningStructuredOutputParser,56)57from langchain_perplexity.types import MediaResponse, WebSearchOptions5859_DictOrPydanticClass: TypeAlias = dict[str, Any] | type[BaseModel]60_DictOrPydantic: TypeAlias = dict | BaseModel6162logger = logging.getLogger(__name__)636465_MODEL_PROFILES = cast("ModelProfileRegistry", _PROFILES)666768def _get_default_model_profile(model_name: str) -> ModelProfile:69 default = _MODEL_PROFILES.get(model_name) or {}70 return default.copy()717273def _is_pydantic_class(obj: Any) -> bool:74 return isinstance(obj, type) and is_basemodel_subclass(obj)757677def _create_usage_metadata(token_usage: dict) -> UsageMetadata:78 """Create UsageMetadata from Perplexity token usage data.7980 Args:81 token_usage: Dictionary containing token usage information from Perplexity API.8283 Returns:84 UsageMetadata with properly structured token counts and details.85 """86 input_tokens = token_usage.get("prompt_tokens", 0)87 output_tokens = token_usage.get("completion_tokens", 0)88 total_tokens = token_usage.get("total_tokens", input_tokens + output_tokens)8990 # Build output_token_details for Perplexity-specific fields91 output_token_details: OutputTokenDetails = {}92 if (reasoning := token_usage.get("reasoning_tokens")) is not None:93 output_token_details["reasoning"] = reasoning94 if (citation_tokens := token_usage.get("citation_tokens")) is not None:95 output_token_details["citation_tokens"] = citation_tokens # type: ignore[typeddict-unknown-key]9697 return UsageMetadata(98 input_tokens=input_tokens,99 output_tokens=output_tokens,100 total_tokens=total_tokens,101 output_token_details=output_token_details,102 )103104105class ChatPerplexity(BaseChatModel):106 """`Perplexity AI` Chat models API.107108 Setup:109 To use, you should have the environment variable `PPLX_API_KEY` set to your API key.110 Any parameters that are valid to be passed to the perplexity.create call111 can be passed in, even if not explicitly saved on this class.112113 ```bash114 export PPLX_API_KEY=your_api_key115 ```116117 Key init args - completion params:118 model:119 Name of the model to use. e.g. "sonar"120 temperature:121 Sampling temperature to use.122 max_tokens:123 Maximum number of tokens to generate.124 streaming:125 Whether to stream the results or not.126127 Key init args - client params:128 pplx_api_key:129 API key for PerplexityChat API.130 request_timeout:131 Timeout for requests to PerplexityChat completion API.132 max_retries:133 Maximum number of retries to make when generating.134135 See full list of supported init args and their descriptions in the params section.136137 Instantiate:138139 ```python140 from langchain_perplexity import ChatPerplexity141142 model = ChatPerplexity(model="sonar", temperature=0.7)143 ```144145 Invoke:146147 ```python148 messages = [("system", "You are a chatbot."), ("user", "Hello!")]149 model.invoke(messages)150 ```151152 Invoke with structured output:153154 ```python155 from pydantic import BaseModel156157158 class StructuredOutput(BaseModel):159 role: str160 content: str161162163 model.with_structured_output(StructuredOutput)164 model.invoke(messages)165 ```166167 Stream:168 ```python169 for chunk in model.stream(messages):170 print(chunk.content)171 ```172173 Token usage:174 ```python175 response = model.invoke(messages)176 response.usage_metadata177 ```178179 Response metadata:180 ```python181 response = model.invoke(messages)182 response.response_metadata183 ```184 """ # noqa: E501185186 client: Any = Field(default=None, exclude=True)187 async_client: Any = Field(default=None, exclude=True)188189 model: str = "sonar"190 """Model name."""191192 temperature: float = 0.7193 """What sampling temperature to use."""194195 model_kwargs: dict[str, Any] = Field(default_factory=dict)196 """Holds any model parameters valid for `create` call not explicitly specified."""197198 pplx_api_key: SecretStr | None = Field(199 default_factory=secret_from_env("PPLX_API_KEY", default=None), alias="api_key"200 )201 """Perplexity API key."""202203 request_timeout: float | tuple[float, float] | None = Field(None, alias="timeout")204 """Timeout for requests to PerplexityChat completion API."""205206 max_retries: int = 6207 """Maximum number of retries to make when generating."""208209 streaming: bool = False210 """Whether to stream the results or not."""211212 max_tokens: int | None = None213 """Maximum number of tokens to generate."""214215 search_mode: Literal["academic", "sec", "web"] | None = None216 """Search mode for specialized content: "academic", "sec", or "web"."""217218 reasoning_effort: Literal["low", "medium", "high"] | None = None219 """Reasoning effort: "low", "medium", or "high" (default)."""220221 language_preference: str | None = None222 """Language preference:"""223224 search_domain_filter: list[str] | None = None225 """Search domain filter: list of domains to filter search results (max 20)."""226227 return_images: bool = False228 """Whether to return images in the response."""229230 return_related_questions: bool = False231 """Whether to return related questions in the response."""232233 search_recency_filter: Literal["day", "week", "month", "year"] | None = None234 """Filter search results by recency: "day", "week", "month", or "year"."""235236 search_after_date_filter: str | None = None237 """Search after date filter: date in format "MM/DD/YYYY" (default)."""238239 search_before_date_filter: str | None = None240 """Only return results before this date (format: MM/DD/YYYY)."""241242 last_updated_after_filter: str | None = None243 """Only return results updated after this date (format: MM/DD/YYYY)."""244245 last_updated_before_filter: str | None = None246 """Only return results updated before this date (format: MM/DD/YYYY)."""247248 disable_search: bool = False249 """Whether to disable web search entirely."""250251 enable_search_classifier: bool = False252 """Whether to enable the search classifier."""253254 web_search_options: WebSearchOptions | None = None255 """Configuration for web search behavior including Pro Search."""256257 media_response: MediaResponse | None = None258 """Media response: "images", "videos", or "none" (default)."""259260 model_config = ConfigDict(populate_by_name=True)261262 @property263 def lc_secrets(self) -> dict[str, str]:264 return {"pplx_api_key": "PPLX_API_KEY"}265266 @model_validator(mode="before")267 @classmethod268 def build_extra(cls, values: dict[str, Any]) -> Any:269 """Build extra kwargs from additional params that were passed in."""270 all_required_field_names = get_pydantic_field_names(cls)271 extra = values.get("model_kwargs", {})272 for field_name in list(values):273 if field_name in extra:274 raise ValueError(f"Found {field_name} supplied twice.")275 if field_name not in all_required_field_names:276 logger.warning(277 f"""WARNING! {field_name} is not a default parameter.278 {field_name} was transferred to model_kwargs.279 Please confirm that {field_name} is what you intended."""280 )281 extra[field_name] = values.pop(field_name)282283 invalid_model_kwargs = all_required_field_names.intersection(extra.keys())284 if invalid_model_kwargs:285 raise ValueError(286 f"Parameters {invalid_model_kwargs} should be specified explicitly. "287 f"Instead they were passed in as part of `model_kwargs` parameter."288 )289290 values["model_kwargs"] = extra291 return values292293 @model_validator(mode="after")294 def validate_environment(self) -> Self:295 """Validate that api key and python package exists in environment."""296 pplx_api_key = (297 self.pplx_api_key.get_secret_value() if self.pplx_api_key else None298 )299300 client_params: dict[str, Any] = {301 "api_key": pplx_api_key,302 "max_retries": self.max_retries,303 }304 if self.request_timeout is not None:305 client_params["timeout"] = self.request_timeout306307 if not self.client:308 self.client = Perplexity(**client_params)309310 if not self.async_client:311 self.async_client = AsyncPerplexity(**client_params)312313 return self314315 def _resolve_model_profile(self) -> ModelProfile | None:316 return _get_default_model_profile(self.model) or None317318 @property319 def _default_params(self) -> dict[str, Any]:320 """Get the default parameters for calling PerplexityChat API."""321 params: dict[str, Any] = {322 "max_tokens": self.max_tokens,323 "stream": self.streaming,324 "temperature": self.temperature,325 }326 if self.search_mode:327 params["search_mode"] = self.search_mode328 if self.reasoning_effort:329 params["reasoning_effort"] = self.reasoning_effort330 if self.language_preference:331 params["language_preference"] = self.language_preference332 if self.search_domain_filter:333 params["search_domain_filter"] = self.search_domain_filter334 if self.return_images:335 params["return_images"] = self.return_images336 if self.return_related_questions:337 params["return_related_questions"] = self.return_related_questions338 if self.search_recency_filter:339 params["search_recency_filter"] = self.search_recency_filter340 if self.search_after_date_filter:341 params["search_after_date_filter"] = self.search_after_date_filter342 if self.search_before_date_filter:343 params["search_before_date_filter"] = self.search_before_date_filter344 if self.last_updated_after_filter:345 params["last_updated_after_filter"] = self.last_updated_after_filter346 if self.last_updated_before_filter:347 params["last_updated_before_filter"] = self.last_updated_before_filter348 if self.disable_search:349 params["disable_search"] = self.disable_search350 if self.enable_search_classifier:351 params["enable_search_classifier"] = self.enable_search_classifier352 if self.web_search_options:353 params["web_search_options"] = self.web_search_options.model_dump(354 exclude_none=True355 )356 if self.media_response:357 if "extra_body" not in params:358 params["extra_body"] = {}359 params["extra_body"]["media_response"] = self.media_response.model_dump(360 exclude_none=True361 )362363 return {**params, **self.model_kwargs}364365 def _convert_message_to_dict(self, message: BaseMessage) -> dict[str, Any]:366 if isinstance(message, ChatMessage):367 message_dict = {"role": message.role, "content": message.content}368 elif isinstance(message, SystemMessage):369 message_dict = {"role": "system", "content": message.content}370 elif isinstance(message, HumanMessage):371 message_dict = {"role": "user", "content": message.content}372 elif isinstance(message, AIMessage):373 message_dict = {"role": "assistant", "content": message.content}374 else:375 raise TypeError(f"Got unknown type {message}")376 return message_dict377378 def _create_message_dicts(379 self, messages: list[BaseMessage], stop: list[str] | None380 ) -> tuple[list[dict[str, Any]], dict[str, Any]]:381 params = dict(self._invocation_params)382 if stop is not None:383 if "stop" in params:384 raise ValueError("`stop` found in both the input and default params.")385 params["stop"] = stop386 message_dicts = [self._convert_message_to_dict(m) for m in messages]387 return message_dicts, params388389 def _convert_delta_to_message_chunk(390 self, _dict: Mapping[str, Any], default_class: type[BaseMessageChunk]391 ) -> BaseMessageChunk:392 role = _dict.get("role")393 content = _dict.get("content") or ""394 additional_kwargs: dict = {}395 if _dict.get("function_call"):396 function_call = dict(_dict["function_call"])397 if "name" in function_call and function_call["name"] is None:398 function_call["name"] = ""399 additional_kwargs["function_call"] = function_call400 if _dict.get("tool_calls"):401 additional_kwargs["tool_calls"] = _dict["tool_calls"]402403 if role == "user" or default_class == HumanMessageChunk:404 return HumanMessageChunk(content=content)405 elif role == "assistant" or default_class == AIMessageChunk:406 return AIMessageChunk(content=content, additional_kwargs=additional_kwargs)407 elif role == "system" or default_class == SystemMessageChunk:408 return SystemMessageChunk(content=content)409 elif role == "function" or default_class == FunctionMessageChunk:410 return FunctionMessageChunk(content=content, name=_dict["name"])411 elif role == "tool" or default_class == ToolMessageChunk:412 return ToolMessageChunk(content=content, tool_call_id=_dict["tool_call_id"])413 elif role or default_class == ChatMessageChunk:414 return ChatMessageChunk(content=content, role=role) # type: ignore[arg-type]415 else:416 return default_class(content=content) # type: ignore[call-arg]417418 def _stream(419 self,420 messages: list[BaseMessage],421 stop: list[str] | None = None,422 run_manager: CallbackManagerForLLMRun | None = None,423 **kwargs: Any,424 ) -> Iterator[ChatGenerationChunk]:425 message_dicts, params = self._create_message_dicts(messages, stop)426 params = {**params, **kwargs}427 default_chunk_class = AIMessageChunk428 params.pop("stream", None)429 if stop:430 params["stop_sequences"] = stop431 stream_resp = self.client.chat.completions.create(432 messages=message_dicts, stream=True, **params433 )434 first_chunk = True435 prev_total_usage: UsageMetadata | None = None436437 added_model_name: bool = False438 added_search_queries: bool = False439 added_search_context_size: bool = False440 for chunk in stream_resp:441 if not isinstance(chunk, dict):442 chunk = chunk.model_dump()443 # Collect standard usage metadata (transform from aggregate to delta)444 if total_usage := chunk.get("usage"):445 lc_total_usage = _create_usage_metadata(total_usage)446 if prev_total_usage:447 usage_metadata: UsageMetadata | None = subtract_usage(448 lc_total_usage, prev_total_usage449 )450 else:451 usage_metadata = lc_total_usage452 prev_total_usage = lc_total_usage453 else:454 usage_metadata = None455 generation_info = {}456 if (model_name := chunk.get("model")) and not added_model_name:457 generation_info["model_name"] = model_name458 added_model_name = True459 if total_usage := chunk.get("usage"):460 if num_search_queries := total_usage.get("num_search_queries"):461 if not added_search_queries:462 generation_info["num_search_queries"] = num_search_queries463 added_search_queries = True464 if not added_search_context_size:465 if search_context_size := total_usage.get("search_context_size"):466 generation_info["search_context_size"] = search_context_size467 added_search_context_size = True468469 choices = chunk.get("choices") or []470 if len(choices) == 0:471 # Usage-only or otherwise empty chunk: still yield so the stream472 # is never empty and downstream callers receive usage metadata.473 message = AIMessageChunk(content="", usage_metadata=usage_metadata)474 yield ChatGenerationChunk(475 message=message, generation_info=generation_info or None476 )477 continue478 choice = choices[0]479480 additional_kwargs = {}481 if first_chunk:482 additional_kwargs["citations"] = chunk.get("citations", [])483 for attr in ["images", "related_questions", "search_results"]:484 if attr in chunk:485 additional_kwargs[attr] = chunk[attr]486487 if chunk.get("videos"):488 additional_kwargs["videos"] = chunk["videos"]489490 if chunk.get("reasoning_steps"):491 additional_kwargs["reasoning_steps"] = chunk["reasoning_steps"]492493 chunk = self._convert_delta_to_message_chunk(494 choice["delta"], default_chunk_class495 )496497 if isinstance(chunk, AIMessageChunk) and usage_metadata:498 chunk.usage_metadata = usage_metadata499500 if first_chunk:501 chunk.additional_kwargs |= additional_kwargs502 first_chunk = False503504 if finish_reason := choice.get("finish_reason"):505 generation_info["finish_reason"] = finish_reason506507 default_chunk_class = chunk.__class__508 chunk = ChatGenerationChunk(message=chunk, generation_info=generation_info)509 if run_manager:510 run_manager.on_llm_new_token(chunk.text, chunk=chunk)511 yield chunk512513 async def _astream(514 self,515 messages: list[BaseMessage],516 stop: list[str] | None = None,517 run_manager: AsyncCallbackManagerForLLMRun | None = None,518 **kwargs: Any,519 ) -> AsyncIterator[ChatGenerationChunk]:520 message_dicts, params = self._create_message_dicts(messages, stop)521 params = {**params, **kwargs}522 default_chunk_class = AIMessageChunk523 params.pop("stream", None)524 if stop:525 params["stop_sequences"] = stop526 stream_resp = await self.async_client.chat.completions.create(527 messages=message_dicts, stream=True, **params528 )529 first_chunk = True530 prev_total_usage: UsageMetadata | None = None531532 added_model_name: bool = False533 added_search_queries: bool = False534 async for chunk in stream_resp:535 if not isinstance(chunk, dict):536 chunk = chunk.model_dump()537 if total_usage := chunk.get("usage"):538 lc_total_usage = _create_usage_metadata(total_usage)539 if prev_total_usage:540 usage_metadata: UsageMetadata | None = subtract_usage(541 lc_total_usage, prev_total_usage542 )543 else:544 usage_metadata = lc_total_usage545 prev_total_usage = lc_total_usage546 else:547 usage_metadata = None548 generation_info = {}549 if (model_name := chunk.get("model")) and not added_model_name:550 generation_info["model_name"] = model_name551 added_model_name = True552 if total_usage := chunk.get("usage"):553 if num_search_queries := total_usage.get("num_search_queries"):554 if not added_search_queries:555 generation_info["num_search_queries"] = num_search_queries556 added_search_queries = True557 if search_context_size := total_usage.get("search_context_size"):558 generation_info["search_context_size"] = search_context_size559560 choices = chunk.get("choices") or []561 if len(choices) == 0:562 # Usage-only or otherwise empty chunk: still yield so the stream563 # is never empty and downstream callers receive usage metadata.564 message = AIMessageChunk(content="", usage_metadata=usage_metadata)565 yield ChatGenerationChunk(566 message=message, generation_info=generation_info or None567 )568 continue569 choice = choices[0]570571 additional_kwargs = {}572 if first_chunk:573 additional_kwargs["citations"] = chunk.get("citations", [])574 for attr in ["images", "related_questions", "search_results"]:575 if attr in chunk:576 additional_kwargs[attr] = chunk[attr]577578 if chunk.get("videos"):579 additional_kwargs["videos"] = chunk["videos"]580581 if chunk.get("reasoning_steps"):582 additional_kwargs["reasoning_steps"] = chunk["reasoning_steps"]583584 chunk = self._convert_delta_to_message_chunk(585 choice["delta"], default_chunk_class586 )587588 if isinstance(chunk, AIMessageChunk) and usage_metadata:589 chunk.usage_metadata = usage_metadata590591 if first_chunk:592 chunk.additional_kwargs |= additional_kwargs593 first_chunk = False594595 if finish_reason := choice.get("finish_reason"):596 generation_info["finish_reason"] = finish_reason597598 default_chunk_class = chunk.__class__599 chunk = ChatGenerationChunk(message=chunk, generation_info=generation_info)600 if run_manager:601 await run_manager.on_llm_new_token(chunk.text, chunk=chunk)602 yield chunk603604 def _generate(605 self,606 messages: list[BaseMessage],607 stop: list[str] | None = None,608 run_manager: CallbackManagerForLLMRun | None = None,609 **kwargs: Any,610 ) -> ChatResult:611 if self.streaming:612 stream_iter = self._stream(613 messages, stop=stop, run_manager=run_manager, **kwargs614 )615 if stream_iter:616 return generate_from_stream(stream_iter)617 message_dicts, params = self._create_message_dicts(messages, stop)618 params = {**params, **kwargs}619 response = self.client.chat.completions.create(messages=message_dicts, **params)620621 if hasattr(response, "usage") and response.usage:622 usage_dict = response.usage.model_dump()623 usage_metadata = _create_usage_metadata(usage_dict)624 else:625 usage_metadata = None626 usage_dict = {}627628 additional_kwargs = {}629 for attr in ["citations", "images", "related_questions", "search_results"]:630 if hasattr(response, attr) and getattr(response, attr):631 additional_kwargs[attr] = getattr(response, attr)632633 if hasattr(response, "videos") and response.videos:634 additional_kwargs["videos"] = [635 v.model_dump() if hasattr(v, "model_dump") else v636 for v in response.videos637 ]638639 if hasattr(response, "reasoning_steps") and response.reasoning_steps:640 additional_kwargs["reasoning_steps"] = [641 r.model_dump() if hasattr(r, "model_dump") else r642 for r in response.reasoning_steps643 ]644645 response_metadata: dict[str, Any] = {646 "model_name": getattr(response, "model", self.model)647 }648 if num_search_queries := usage_dict.get("num_search_queries"):649 response_metadata["num_search_queries"] = num_search_queries650 if search_context_size := usage_dict.get("search_context_size"):651 response_metadata["search_context_size"] = search_context_size652653 message = AIMessage(654 content=response.choices[0].message.content,655 additional_kwargs=additional_kwargs,656 usage_metadata=usage_metadata,657 response_metadata=response_metadata,658 )659 return ChatResult(generations=[ChatGeneration(message=message)])660661 async def _agenerate(662 self,663 messages: list[BaseMessage],664 stop: list[str] | None = None,665 run_manager: AsyncCallbackManagerForLLMRun | None = None,666 **kwargs: Any,667 ) -> ChatResult:668 if self.streaming:669 stream_iter = self._astream(670 messages, stop=stop, run_manager=run_manager, **kwargs671 )672 if stream_iter:673 return await agenerate_from_stream(stream_iter)674 message_dicts, params = self._create_message_dicts(messages, stop)675 params = {**params, **kwargs}676 response = await self.async_client.chat.completions.create(677 messages=message_dicts, **params678 )679680 if hasattr(response, "usage") and response.usage:681 usage_dict = response.usage.model_dump()682 usage_metadata = _create_usage_metadata(usage_dict)683 else:684 usage_metadata = None685 usage_dict = {}686687 additional_kwargs = {}688 for attr in ["citations", "images", "related_questions", "search_results"]:689 if hasattr(response, attr) and getattr(response, attr):690 additional_kwargs[attr] = getattr(response, attr)691692 if hasattr(response, "videos") and response.videos:693 additional_kwargs["videos"] = [694 v.model_dump() if hasattr(v, "model_dump") else v695 for v in response.videos696 ]697698 if hasattr(response, "reasoning_steps") and response.reasoning_steps:699 additional_kwargs["reasoning_steps"] = [700 r.model_dump() if hasattr(r, "model_dump") else r701 for r in response.reasoning_steps702 ]703704 response_metadata: dict[str, Any] = {705 "model_name": getattr(response, "model", self.model)706 }707 if num_search_queries := usage_dict.get("num_search_queries"):708 response_metadata["num_search_queries"] = num_search_queries709 if search_context_size := usage_dict.get("search_context_size"):710 response_metadata["search_context_size"] = search_context_size711712 message = AIMessage(713 content=response.choices[0].message.content,714 additional_kwargs=additional_kwargs,715 usage_metadata=usage_metadata,716 response_metadata=response_metadata,717 )718 return ChatResult(generations=[ChatGeneration(message=message)])719720 @property721 def _invocation_params(self) -> Mapping[str, Any]:722 """Get the parameters used to invoke the model."""723 pplx_creds: dict[str, Any] = {"model": self.model}724 return {**pplx_creds, **self._default_params}725726 @property727 def _llm_type(self) -> str:728 """Return type of chat model."""729 return "perplexitychat"730731 def with_structured_output(732 self,733 schema: _DictOrPydanticClass | None = None,734 *,735 method: Literal["json_schema"] = "json_schema",736 include_raw: bool = False,737 strict: bool | None = None,738 **kwargs: Any,739 ) -> Runnable[LanguageModelInput, _DictOrPydantic]:740 """Model wrapper that returns outputs formatted to match the given schema for Preplexity.741 Currently, Perplexity only supports "json_schema" method for structured output742 as per their [official documentation](https://docs.perplexity.ai/guides/structured-outputs).743744 Args:745 schema: The output schema. Can be passed in as:746747 - a JSON Schema,748 - a `TypedDict` class,749 - or a Pydantic class750751 method: The method for steering model generation, currently only support:752753 - `'json_schema'`: Use the JSON Schema to parse the model output754755756 include_raw:757 If `False` then only the parsed structured output is returned.758759 If an error occurs during model output parsing it will be raised.760761 If `True` then both the raw model response (a `BaseMessage`) and the762 parsed model response will be returned.763764 If an error occurs during output parsing it will be caught and returned765 as well.766767 The final output is always a `dict` with keys `'raw'`, `'parsed'`, and768 `'parsing_error'`.769 strict:770 Unsupported: whether to enable strict schema adherence when generating771 the output. This parameter is included for compatibility with other772 chat models, but is currently ignored.773774 kwargs: Additional keyword args aren't supported.775776 Returns:777 A `Runnable` that takes same inputs as a778 `langchain_core.language_models.chat.BaseChatModel`. If `include_raw` is779 `False` and `schema` is a Pydantic class, `Runnable` outputs an instance780 of `schema` (i.e., a Pydantic object). Otherwise, if `include_raw` is781 `False` then `Runnable` outputs a `dict`.782783 If `include_raw` is `True`, then `Runnable` outputs a `dict` with keys:784785 - `'raw'`: `BaseMessage`786 - `'parsed'`: `None` if there was a parsing error, otherwise the type787 depends on the `schema` as described above.788 - `'parsing_error'`: `BaseException | None`789 """ # noqa: E501790 if method in ("function_calling", "json_mode"):791 method = "json_schema"792 if method == "json_schema":793 if schema is None:794 raise ValueError(795 "schema must be specified when method is not 'json_schema'. "796 "Received None."797 )798 is_pydantic_schema = _is_pydantic_class(schema)799 response_format = convert_to_json_schema(schema)800 llm = self.bind(801 response_format={802 "type": "json_schema",803 "json_schema": {"schema": response_format},804 },805 ls_structured_output_format={806 "kwargs": {"method": method},807 "schema": response_format,808 },809 )810 output_parser = (811 ReasoningStructuredOutputParser(pydantic_object=schema) # type: ignore[arg-type]812 if is_pydantic_schema813 else ReasoningJsonOutputParser()814 )815 else:816 raise ValueError(817 f"Unrecognized method argument. Expected 'json_schema' Received:\818 '{method}'"819 )820821 if include_raw:822 parser_assign = RunnablePassthrough.assign(823 parsed=itemgetter("raw") | output_parser, parsing_error=lambda _: None824 )825 parser_none = RunnablePassthrough.assign(parsed=lambda _: None)826 parser_with_fallback = parser_assign.with_fallbacks(827 [parser_none], exception_key="parsing_error"828 )829 return RunnableMap(raw=llm) | parser_with_fallback830 else:831 return llm | output_parser
Same data, no extra tab — call code_get_file + code_get_findings over MCP from Claude/Cursor/Copilot.