Catch specific exceptions instead of Exception to avoid masking bugs
except Exception:
1"""AI message."""23import itertools4import json5import logging6import operator7from collections.abc import Sequence8from typing import Any, Literal, cast, overload910from pydantic import Field, model_validator11from typing_extensions import NotRequired, Self, TypedDict, override1213from langchain_core.messages import content as types14from langchain_core.messages.base import (15 BaseMessage,16 BaseMessageChunk,17 _extract_reasoning_from_additional_kwargs,18 merge_content,19)20from langchain_core.messages.content import InvalidToolCall21from langchain_core.messages.tool import (22 ToolCall,23 ToolCallChunk,24 default_tool_chunk_parser,25 default_tool_parser,26)27from langchain_core.messages.tool import invalid_tool_call as create_invalid_tool_call28from langchain_core.messages.tool import tool_call as create_tool_call29from langchain_core.messages.tool import tool_call_chunk as create_tool_call_chunk30from langchain_core.utils._merge import merge_dicts, merge_lists31from langchain_core.utils.json import parse_partial_json32from langchain_core.utils.usage import _dict_int_op33from langchain_core.utils.utils import LC_AUTO_PREFIX, LC_ID_PREFIX3435logger = logging.getLogger(__name__)363738class InputTokenDetails(TypedDict, total=False):39 """Breakdown of input token counts.4041 Does *not* need to sum to full input token count. Does *not* need to have all keys.4243 Example:44 ```python45 {46 "audio": 10,47 "cache_creation": 200,48 "cache_read": 100,49 }50 ```5152 May also hold extra provider-specific keys.5354 !!! version-added "Added in `langchain-core` 0.3.9"55 """5657 audio: int58 """Audio input tokens."""5960 cache_creation: int61 """Input tokens that were cached and there was a cache miss.6263 Since there was a cache miss, the cache was created from these tokens.64 """6566 cache_read: int67 """Input tokens that were cached and there was a cache hit.6869 Since there was a cache hit, the tokens were read from the cache. More precisely,70 the model state given these tokens was read from the cache.71 """727374class OutputTokenDetails(TypedDict, total=False):75 """Breakdown of output token counts.7677 Does *not* need to sum to full output token count. Does *not* need to have all keys.7879 Example:80 ```python81 {82 "audio": 10,83 "reasoning": 200,84 }85 ```8687 May also hold extra provider-specific keys.8889 !!! version-added "Added in `langchain-core` 0.3.9"9091 """9293 audio: int94 """Audio output tokens."""9596 reasoning: int97 """Reasoning output tokens.9899 Tokens generated by the model in a chain of thought process that are not100 returned as part of model output.101 """102103104class UsageMetadata(TypedDict):105 """Usage metadata for a message, such as token counts.106107 This is a standard representation of token usage that is consistent across models.108109 Example:110 ```python111 {112 "input_tokens": 350,113 "output_tokens": 240,114 "total_tokens": 590,115 "input_token_details": {116 "audio": 10,117 "cache_creation": 200,118 "cache_read": 100,119 },120 "output_token_details": {121 "audio": 10,122 "reasoning": 200,123 },124 }125 ```126127 !!! warning "Behavior changed in `langchain-core` 0.3.9"128129 Added `input_token_details` and `output_token_details`.130131 !!! note "LangSmith SDK"132133 The LangSmith SDK also has a `UsageMetadata` class. While the two share fields,134 LangSmith's `UsageMetadata` has additional fields to capture cost information135 used by the LangSmith platform.136 """137138 input_tokens: int139 """Count of input (or prompt) tokens. Sum of all input token types."""140141 output_tokens: int142 """Count of output (or completion) tokens. Sum of all output token types."""143144 total_tokens: int145 """Total token count. Sum of `input_tokens` + `output_tokens`."""146147 input_token_details: NotRequired[InputTokenDetails]148 """Breakdown of input token counts.149150 Does *not* need to sum to full input token count. Does *not* need to have all keys.151 """152153 output_token_details: NotRequired[OutputTokenDetails]154 """Breakdown of output token counts.155156 Does *not* need to sum to full output token count. Does *not* need to have all keys.157 """158159160class AIMessage(BaseMessage):161 """Message from an AI.162163 An `AIMessage` is returned from a chat model as a response to a prompt.164165 This message represents the output of the model and consists of both166 the raw output as returned by the model and standardized fields167 (e.g., tool calls, usage metadata) added by the LangChain framework.168 """169170 tool_calls: list[ToolCall] = Field(default_factory=list)171 """If present, tool calls associated with the message."""172173 invalid_tool_calls: list[InvalidToolCall] = Field(default_factory=list)174 """If present, tool calls with parsing errors associated with the message."""175176 usage_metadata: UsageMetadata | None = None177 """If present, usage metadata for a message, such as token counts.178179 This is a standard representation of token usage that is consistent across models.180 """181182 type: Literal["ai"] = "ai"183 """The type of the message (used for deserialization)."""184185 @overload186 def __init__(187 self,188 content: str | list[str | dict[Any, Any]],189 **kwargs: Any,190 ) -> None: ...191192 @overload193 def __init__(194 self,195 content: str | list[str | dict[Any, Any]] | None = None,196 content_blocks: list[types.ContentBlock] | None = None,197 **kwargs: Any,198 ) -> None: ...199200 def __init__(201 self,202 content: str | list[str | dict[Any, Any]] | None = None,203 content_blocks: list[types.ContentBlock] | None = None,204 **kwargs: Any,205 ) -> None:206 """Initialize an `AIMessage`.207208 Specify `content` as positional arg or `content_blocks` for typing.209210 Args:211 content: The content of the message.212 content_blocks: Typed standard content.213 **kwargs: Additional arguments to pass to the parent class.214 """215 if content_blocks is not None:216 # If there are tool calls in content_blocks, but not in tool_calls, add them217 content_tool_calls = [218 block for block in content_blocks if block.get("type") == "tool_call"219 ]220 if content_tool_calls and "tool_calls" not in kwargs:221 kwargs["tool_calls"] = content_tool_calls222223 super().__init__(224 content=cast("list[str | dict[Any, Any]]", content_blocks),225 **kwargs,226 )227 else:228 super().__init__(content=content, **kwargs)229230 @property231 def lc_attributes(self) -> dict[str, Any]:232 """Attributes to be serialized.233234 Includes all attributes, even if they are derived from other initialization235 arguments.236 """237 return {238 "tool_calls": self.tool_calls,239 "invalid_tool_calls": self.invalid_tool_calls,240 }241242 @property243 def content_blocks(self) -> list[types.ContentBlock]:244 """Return standard, typed `ContentBlock` dicts from the message.245246 If the message has a known model provider, use the provider-specific translator247 first before falling back to best-effort parsing. For details, see the property248 on `BaseMessage`.249 """250 if self.response_metadata.get("output_version") == "v1" and isinstance(251 self.content, list252 ):253 # Only short-circuit when content is a list (assumed under v1 to254 # already hold ContentBlock dicts; the cast is unchecked). See255 # AIMessageChunk.content_blocks for full rationale.256 return cast("list[types.ContentBlock]", self.content)257258 model_provider = self.response_metadata.get("model_provider")259 if model_provider:260 from langchain_core.messages.block_translators import ( # noqa: PLC0415261 get_translator,262 )263264 translator = get_translator(model_provider)265 if translator:266 try:267 return translator["translate_content"](self)268 except NotImplementedError:269 pass270271 # Otherwise, use best-effort parsing272 blocks = super().content_blocks273274 if self.tool_calls:275 # Add from tool_calls if missing from content276 content_tool_call_ids = {277 block.get("id")278 for block in self.content279 if isinstance(block, dict) and block.get("type") == "tool_call"280 }281 for tool_call in self.tool_calls:282 if (id_ := tool_call.get("id")) and id_ not in content_tool_call_ids:283 tool_call_block: types.ToolCall = {284 "type": "tool_call",285 "id": id_,286 "name": tool_call["name"],287 "args": tool_call["args"],288 }289 if "index" in tool_call:290 tool_call_block["index"] = tool_call["index"] # type: ignore[typeddict-item]291 if "extras" in tool_call:292 tool_call_block["extras"] = tool_call["extras"] # type: ignore[typeddict-item]293 blocks.append(tool_call_block)294295 # Best-effort reasoning extraction from additional_kwargs296 # Only add reasoning if not already present297 # Insert before all other blocks to keep reasoning at the start298 has_reasoning = any(block.get("type") == "reasoning" for block in blocks)299 if not has_reasoning and (300 reasoning_block := _extract_reasoning_from_additional_kwargs(self)301 ):302 blocks.insert(0, reasoning_block)303304 return blocks305306 # TODO: remove this logic if possible, reducing breaking nature of changes307 @model_validator(mode="before")308 @classmethod309 def _backwards_compat_tool_calls(cls, values: dict[str, Any]) -> Any:310 check_additional_kwargs = not any(311 values.get(k)312 for k in ("tool_calls", "invalid_tool_calls", "tool_call_chunks")313 )314 if check_additional_kwargs and (315 raw_tool_calls := values.get("additional_kwargs", {}).get("tool_calls")316 ):317 try:318 if issubclass(cls, AIMessageChunk):319 values["tool_call_chunks"] = default_tool_chunk_parser(320 raw_tool_calls321 )322 else:323 parsed_tool_calls, parsed_invalid_tool_calls = default_tool_parser(324 raw_tool_calls325 )326 values["tool_calls"] = parsed_tool_calls327 values["invalid_tool_calls"] = parsed_invalid_tool_calls328 except Exception:329 logger.debug("Failed to parse tool calls", exc_info=True)330331 # Ensure "type" is properly set on all tool call-like dicts.332 if tool_calls := values.get("tool_calls"):333 values["tool_calls"] = [334 create_tool_call(335 **{k: v for k, v in tc.items() if k not in {"type", "extras"}}336 )337 for tc in tool_calls338 ]339 if invalid_tool_calls := values.get("invalid_tool_calls"):340 values["invalid_tool_calls"] = [341 create_invalid_tool_call(**{k: v for k, v in tc.items() if k != "type"})342 for tc in invalid_tool_calls343 ]344345 if tool_call_chunks := values.get("tool_call_chunks"):346 values["tool_call_chunks"] = [347 create_tool_call_chunk(**{k: v for k, v in tc.items() if k != "type"})348 for tc in tool_call_chunks349 ]350351 return values352353 @override354 def pretty_repr(self, html: bool = False) -> str:355 """Return a pretty representation of the message for display.356357 Args:358 html: Whether to return an HTML-formatted string.359360 Returns:361 A pretty representation of the message.362363 Example:364 ```python365 from langchain_core.messages import AIMessage366367 msg = AIMessage(368 content="Let me check the weather.",369 tool_calls=[370 {"name": "get_weather", "args": {"city": "Paris"}, "id": "1"}371 ],372 )373 ```374375 Results in:376 ```python377 >>> print(msg.pretty_repr())378 ================================== Ai Message ==================================379380 Let me check the weather.381 Tool Calls:382 get_weather (1)383 Call ID: 1384 Args:385 city: Paris386 ```387 """ # noqa: E501388 base = super().pretty_repr(html=html)389 lines = []390391 def _format_tool_args(tc: ToolCall | InvalidToolCall) -> list[str]:392 lines = [393 f" {tc.get('name', 'Tool')} ({tc.get('id')})",394 f" Call ID: {tc.get('id')}",395 ]396 if tc.get("error"):397 lines.append(f" Error: {tc.get('error')}")398 lines.append(" Args:")399 args = tc.get("args")400 if isinstance(args, str):401 lines.append(f" {args}")402 elif isinstance(args, dict):403 for arg, value in args.items():404 lines.append(f" {arg}: {value}")405 return lines406407 if self.tool_calls:408 lines.append("Tool Calls:")409 for tc in self.tool_calls:410 lines.extend(_format_tool_args(tc))411 if self.invalid_tool_calls:412 lines.append("Invalid Tool Calls:")413 for itc in self.invalid_tool_calls:414 lines.extend(_format_tool_args(itc))415 return (base.strip() + "\n" + "\n".join(lines)).strip()416417418class AIMessageChunk(AIMessage, BaseMessageChunk):419 """Message chunk from an AI (yielded when streaming)."""420421 # Ignoring mypy re-assignment here since we're overriding the value422 # to make sure that the chunk variant can be discriminated from the423 # non-chunk variant.424 type: Literal["AIMessageChunk"] = "AIMessageChunk" # type: ignore[assignment]425 """The type of the message (used for deserialization)."""426427 tool_call_chunks: list[ToolCallChunk] = Field(default_factory=list)428 """If provided, tool call chunks associated with the message."""429430 chunk_position: Literal["last"] | None = None431 """Optional span represented by an aggregated `AIMessageChunk`.432433 If a chunk with `chunk_position="last"` is aggregated into a stream,434 `tool_call_chunks` in message content will be parsed into `tool_calls`.435 """436437 @property438 @override439 def lc_attributes(self) -> dict[str, Any]:440 return {441 "tool_calls": self.tool_calls,442 "invalid_tool_calls": self.invalid_tool_calls,443 }444445 @property446 def content_blocks(self) -> list[types.ContentBlock]:447 """Return standard, typed `ContentBlock` dicts from the message."""448 if self.response_metadata.get("output_version") == "v1" and isinstance(449 self.content, list450 ):451 # Only short-circuit when content is already a list of ContentBlock452 # dicts. Some streaming implementations keep content as a string453 # even when output_version="v1" is set (e.g., OpenAI Chat454 # Completions), so it must fall through to the model_provider455 # translator which builds ContentBlock dicts from tool_calls /456 # tool_call_chunks. Without this guard, string content would be457 # returned directly, silently dropping tool calls.458 return cast("list[types.ContentBlock]", self.content)459460 model_provider = self.response_metadata.get("model_provider")461 if model_provider:462 from langchain_core.messages.block_translators import ( # noqa: PLC0415463 get_translator,464 )465466 translator = get_translator(model_provider)467 if translator:468 try:469 return translator["translate_content_chunk"](self)470 except NotImplementedError:471 pass472473 # Otherwise, use best-effort parsing474 blocks = super().content_blocks475476 if (477 self.tool_call_chunks478 and not self.content479 and self.chunk_position != "last" # keep tool_calls if aggregated480 ):481 blocks = [482 block483 for block in blocks484 if block["type"] not in {"tool_call", "invalid_tool_call"}485 ]486 for tool_call_chunk in self.tool_call_chunks:487 tc: types.ToolCallChunk = {488 "type": "tool_call_chunk",489 "id": tool_call_chunk.get("id"),490 "name": tool_call_chunk.get("name"),491 "args": tool_call_chunk.get("args"),492 }493 if (idx := tool_call_chunk.get("index")) is not None:494 tc["index"] = idx495 blocks.append(tc)496497 # Best-effort reasoning extraction from additional_kwargs498 # Only add reasoning if not already present499 # Insert before all other blocks to keep reasoning at the start500 has_reasoning = any(block.get("type") == "reasoning" for block in blocks)501 if not has_reasoning and (502 reasoning_block := _extract_reasoning_from_additional_kwargs(self)503 ):504 blocks.insert(0, reasoning_block)505506 return blocks507508 @model_validator(mode="after")509 def init_tool_calls(self) -> Self:510 """Initialize tool calls from tool call chunks.511512 Returns:513 The values with tool calls initialized.514515 Raises:516 ValueError: If the tool call chunks are malformed.517 """518 if not self.tool_call_chunks:519 if self.tool_calls:520 self.tool_call_chunks = [521 create_tool_call_chunk(522 name=tc["name"],523 args=json.dumps(tc["args"]),524 id=tc["id"],525 index=None,526 )527 for tc in self.tool_calls528 ]529 if self.invalid_tool_calls:530 tool_call_chunks = self.tool_call_chunks531 tool_call_chunks.extend(532 [533 create_tool_call_chunk(534 name=tc["name"], args=tc["args"], id=tc["id"], index=None535 )536 for tc in self.invalid_tool_calls537 ]538 )539 self.tool_call_chunks = tool_call_chunks540541 return self542 tool_calls = []543 invalid_tool_calls = []544545 def add_chunk_to_invalid_tool_calls(chunk: ToolCallChunk) -> None:546 invalid_tool_calls.append(547 create_invalid_tool_call(548 name=chunk["name"],549 args=chunk["args"],550 id=chunk["id"],551 error=None,552 )553 )554555 for chunk in self.tool_call_chunks:556 try:557 args_ = parse_partial_json(chunk["args"]) if chunk["args"] else {}558 if isinstance(args_, dict):559 tool_calls.append(560 create_tool_call(561 name=chunk["name"] or "",562 args=args_,563 id=chunk["id"],564 )565 )566 else:567 add_chunk_to_invalid_tool_calls(chunk)568 except Exception:569 add_chunk_to_invalid_tool_calls(chunk)570 self.tool_calls = tool_calls571 self.invalid_tool_calls = invalid_tool_calls572573 if (574 self.chunk_position == "last"575 and self.tool_call_chunks576 and self.response_metadata.get("output_version") == "v1"577 and isinstance(self.content, list)578 ):579 id_to_tc: dict[str, types.ToolCall] = {580 cast("str", tc.get("id")): {581 "type": "tool_call",582 "name": tc["name"],583 "args": tc["args"],584 "id": tc.get("id"),585 }586 for tc in self.tool_calls587 if "id" in tc588 }589 for idx, block in enumerate(self.content):590 if (591 isinstance(block, dict)592 and block.get("type") == "tool_call_chunk"593 and (call_id := block.get("id"))594 and call_id in id_to_tc595 ):596 self.content[idx] = cast("dict[str, Any]", id_to_tc[call_id])597 if "extras" in block:598 # mypy does not account for instance check for dict above599 self.content[idx]["extras"] = block["extras"] # type: ignore[index]600601 return self602603 @model_validator(mode="after")604 def init_server_tool_calls(self) -> Self:605 """Initialize server tool calls.606607 Parse `server_tool_call_chunks` from608 [`ServerToolCallChunk`][langchain.messages.ServerToolCallChunk] objects.609 """610 if (611 self.chunk_position == "last"612 and self.response_metadata.get("output_version") == "v1"613 and isinstance(self.content, list)614 ):615 for idx, block in enumerate(self.content):616 if (617 isinstance(block, dict)618 and block.get("type")619 in {"server_tool_call", "server_tool_call_chunk"}620 and isinstance(args_str := block.get("args") or "{}", str)621 ):622 try:623 args = json.loads(args_str)624 if isinstance(args, dict):625 self.content[idx]["type"] = "server_tool_call" # type: ignore[index]626 self.content[idx]["args"] = args # type: ignore[index]627 except json.JSONDecodeError:628 pass629 return self630631 @overload # type: ignore[override] # summing BaseMessages gives ChatPromptTemplate632 def __add__(self, other: "AIMessageChunk") -> "AIMessageChunk": ...633634 @overload635 def __add__(self, other: Sequence["AIMessageChunk"]) -> "AIMessageChunk": ...636637 @overload638 def __add__(self, other: Any) -> BaseMessageChunk: ...639640 @override641 def __add__(self, other: Any) -> BaseMessageChunk:642 if isinstance(other, AIMessageChunk):643 return add_ai_message_chunks(self, other)644 if isinstance(other, (list, tuple)) and all(645 isinstance(o, AIMessageChunk) for o in other646 ):647 return add_ai_message_chunks(self, *other)648 return super().__add__(other)649650651def add_ai_message_chunks(652 left: AIMessageChunk, *others: AIMessageChunk653) -> AIMessageChunk:654 """Add multiple `AIMessageChunk`s together.655656 Args:657 left: The first `AIMessageChunk`.658 *others: Other `AIMessageChunk`s to add.659660 Returns:661 The resulting `AIMessageChunk`.662663 """664 content = merge_content(left.content, *(o.content for o in others))665 additional_kwargs = merge_dicts(666 left.additional_kwargs, *(o.additional_kwargs for o in others)667 )668 response_metadata = merge_dicts(669 left.response_metadata, *(o.response_metadata for o in others)670 )671672 # Merge tool call chunks673 if raw_tool_calls := merge_lists(674 left.tool_call_chunks, *(o.tool_call_chunks for o in others)675 ):676 tool_call_chunks = [677 create_tool_call_chunk(678 name=rtc.get("name"),679 args=rtc.get("args"),680 index=rtc.get("index"),681 id=rtc.get("id"),682 )683 for rtc in raw_tool_calls684 ]685 else:686 tool_call_chunks = []687688 # Token usage689 if left.usage_metadata or any(o.usage_metadata is not None for o in others):690 usage_metadata: UsageMetadata | None = left.usage_metadata691 for other in others:692 usage_metadata = add_usage(usage_metadata, other.usage_metadata)693 else:694 usage_metadata = None695696 # Ranks are defined by the order of preference. Higher is better:697 # 2. Provider-assigned IDs (non lc_* and non lc_run-*)698 # 1. lc_run-* IDs699 # 0. lc_* and other remaining IDs700 best_rank = -1701 chunk_id = None702 candidates = itertools.chain([left.id], (o.id for o in others))703704 for id_ in candidates:705 if not id_:706 continue707708 if not id_.startswith(LC_ID_PREFIX) and not id_.startswith(LC_AUTO_PREFIX):709 chunk_id = id_710 # Highest rank, return instantly711 break712713 rank = 1 if id_.startswith(LC_ID_PREFIX) else 0714715 if rank > best_rank:716 best_rank = rank717 chunk_id = id_718719 chunk_position: Literal["last"] | None = (720 "last" if any(x.chunk_position == "last" for x in [left, *others]) else None721 )722723 return left.__class__(724 content=content,725 additional_kwargs=additional_kwargs,726 tool_call_chunks=tool_call_chunks,727 response_metadata=response_metadata,728 usage_metadata=usage_metadata,729 id=chunk_id,730 chunk_position=chunk_position,731 )732733734def add_usage(left: UsageMetadata | None, right: UsageMetadata | None) -> UsageMetadata:735 """Recursively add two UsageMetadata objects.736737 Example:738 ```python739 from langchain_core.messages.ai import add_usage740741 left = UsageMetadata(742 input_tokens=5,743 output_tokens=0,744 total_tokens=5,745 input_token_details=InputTokenDetails(cache_read=3),746 )747 right = UsageMetadata(748 input_tokens=0,749 output_tokens=10,750 total_tokens=10,751 output_token_details=OutputTokenDetails(reasoning=4),752 )753754 add_usage(left, right)755 ```756757 results in758759 ```python760 UsageMetadata(761 input_tokens=5,762 output_tokens=10,763 total_tokens=15,764 input_token_details=InputTokenDetails(cache_read=3),765 output_token_details=OutputTokenDetails(reasoning=4),766 )767 ```768 Args:769 left: The first `UsageMetadata` object.770 right: The second `UsageMetadata` object.771772 Returns:773 The sum of the two `UsageMetadata` objects.774775 """776 if not (left or right):777 return UsageMetadata(input_tokens=0, output_tokens=0, total_tokens=0)778 if not (left and right):779 return cast("UsageMetadata", left or right)780781 return UsageMetadata(782 **cast(783 "UsageMetadata",784 _dict_int_op(785 cast("dict[str, Any]", left),786 cast("dict[str, Any]", right),787 operator.add,788 ),789 )790 )791792793def subtract_usage(794 left: UsageMetadata | None, right: UsageMetadata | None795) -> UsageMetadata:796 """Recursively subtract two `UsageMetadata` objects.797798 Token counts cannot be negative so the actual operation is `max(left - right, 0)`.799800 Example:801 ```python802 from langchain_core.messages.ai import subtract_usage803804 left = UsageMetadata(805 input_tokens=5,806 output_tokens=10,807 total_tokens=15,808 input_token_details=InputTokenDetails(cache_read=4),809 )810 right = UsageMetadata(811 input_tokens=3,812 output_tokens=8,813 total_tokens=11,814 output_token_details=OutputTokenDetails(reasoning=4),815 )816817 subtract_usage(left, right)818 ```819820 results in821822 ```python823 UsageMetadata(824 input_tokens=2,825 output_tokens=2,826 total_tokens=4,827 input_token_details=InputTokenDetails(cache_read=4),828 output_token_details=OutputTokenDetails(reasoning=0),829 )830 ```831 Args:832 left: The first `UsageMetadata` object.833 right: The second `UsageMetadata` object.834835 Returns:836 The resulting `UsageMetadata` after subtraction.837838 """839 if not (left or right):840 return UsageMetadata(input_tokens=0, output_tokens=0, total_tokens=0)841 if not (left and right):842 return cast("UsageMetadata", left or right)843844 return UsageMetadata(845 **cast(846 "UsageMetadata",847 _dict_int_op(848 cast("dict[str, Any]", left),849 cast("dict[str, Any]", right),850 (lambda le, ri: max(le - ri, 0)),851 ),852 )853 )
Same data, no extra tab — call code_get_file + code_get_findings over MCP from Claude/Cursor/Copilot.
Generated by searchcode.com · powered by code_get_file + code_get_findings