Avoid global variables; use function parameters or class attributes for better scope management
global _GLOBAL_DEPRECATION_WARNED # noqa: PLW0603
1"""Callback handler that writes to a file."""23from __future__ import annotations45from pathlib import Path6from typing import TYPE_CHECKING, Any, TextIO, cast78from typing_extensions import Self, override910from langchain_core._api import warn_deprecated11from langchain_core.callbacks import BaseCallbackHandler12from langchain_core.utils.input import print_text1314if TYPE_CHECKING:15 from langchain_core.agents import AgentAction, AgentFinish161718_GLOBAL_DEPRECATION_WARNED = False192021class FileCallbackHandler(BaseCallbackHandler):22 """Callback handler that writes to a file.2324 This handler supports both context manager usage (recommended) and direct25 instantiation (deprecated) for backwards compatibility.2627 Examples:28 Using as a context manager (recommended):2930 ```python31 with FileCallbackHandler("output.txt") as handler:32 # Use handler with your chain/agent33 chain.invoke(inputs, config={"callbacks": [handler]})34 ```3536 Direct instantiation (deprecated):3738 ```python39 handler = FileCallbackHandler("output.txt")40 # File remains open until handler is garbage collected41 try:42 chain.invoke(inputs, config={"callbacks": [handler]})43 finally:44 handler.close() # Explicit cleanup recommended45 ```4647 Args:48 filename: The file path to write to.49 mode: The file open mode. Defaults to `'a'` (append).50 color: Default color for text output.5152 !!! note5354 When not used as a context manager, a deprecation warning will be issued on55 first use. The file will be opened immediately in `__init__` and closed in56 `__del__` or when `close()` is called explicitly.5758 """5960 def __init__(61 self, filename: str, mode: str = "a", color: str | None = None62 ) -> None:63 """Initialize the file callback handler.6465 Args:66 filename: Path to the output file.67 mode: File open mode (e.g., `'w'`, `'a'`, `'x'`). Defaults to `'a'`.68 color: Default text color for output.6970 """71 self.filename = filename72 self.mode = mode73 self.color = color74 self._file_opened_in_context = False75 self.file: TextIO = cast(76 "TextIO",77 # Open the file in the specified mode with UTF-8 encoding.78 Path(self.filename).open(self.mode, encoding="utf-8"), # noqa: SIM11579 )8081 def __enter__(self) -> Self:82 """Enter the context manager.8384 Returns:85 The `FileCallbackHandler` instance.8687 !!! note8889 The file is already opened in `__init__`, so this just marks that the90 handler is being used as a context manager.9192 """93 self._file_opened_in_context = True94 return self9596 def __exit__(97 self,98 exc_type: type[BaseException] | None,99 exc_val: BaseException | None,100 exc_tb: object,101 ) -> None:102 """Exit the context manager and close the file.103104 Args:105 exc_type: Exception type if an exception occurred.106 exc_val: Exception value if an exception occurred.107 exc_tb: Exception traceback if an exception occurred.108109 """110 self.close()111112 def __del__(self) -> None:113 """Destructor to cleanup when done."""114 self.close()115116 def close(self) -> None:117 """Close the file if it's open.118119 This method is safe to call multiple times and will only close120 the file if it's currently open.121122 """123 if hasattr(self, "file") and self.file and not self.file.closed:124 self.file.close()125126 def _write(127 self,128 text: str,129 color: str | None = None,130 end: str = "",131 ) -> None:132 """Write text to the file with deprecation warning if needed.133134 Args:135 text: The text to write to the file.136 color: Optional color for the text. Defaults to `self.color`.137 end: String appended after the text.138 file: Optional file to write to. Defaults to `self.file`.139140 Raises:141 RuntimeError: If the file is closed or not available.142143 """144 global _GLOBAL_DEPRECATION_WARNED # noqa: PLW0603145 if not self._file_opened_in_context and not _GLOBAL_DEPRECATION_WARNED:146 warn_deprecated(147 since="0.3.67",148 pending=True,149 message=(150 "Using FileCallbackHandler without a context manager is "151 "deprecated. Use 'with FileCallbackHandler(...) as "152 "handler:' instead."153 ),154 )155 _GLOBAL_DEPRECATION_WARNED = True156157 if not hasattr(self, "file") or self.file is None or self.file.closed:158 msg = "File is not open. Use FileCallbackHandler as a context manager."159 raise RuntimeError(msg)160161 print_text(text, file=self.file, color=color, end=end)162163 @override164 def on_chain_start(165 self, serialized: dict[str, Any], inputs: dict[str, Any], **kwargs: Any166 ) -> None:167 """Print that we are entering a chain.168169 Args:170 serialized: The serialized chain information.171 inputs: The inputs to the chain.172 **kwargs: Additional keyword arguments that may contain `'name'`.173174 """175 name = (176 kwargs.get("name")177 or serialized.get("name", serialized.get("id", ["<unknown>"])[-1])178 or "<unknown>"179 )180 self._write(f"\n\n> Entering new {name} chain...", end="\n")181182 @override183 def on_chain_end(self, outputs: dict[str, Any], **kwargs: Any) -> None:184 """Print that we finished a chain.185186 Args:187 outputs: The outputs of the chain.188 **kwargs: Additional keyword arguments.189190 """191 self._write("\n> Finished chain.", end="\n")192193 @override194 def on_agent_action(195 self, action: AgentAction, color: str | None = None, **kwargs: Any196 ) -> Any:197 """Handle agent action by writing the action log.198199 Args:200 action: The agent action containing the log to write.201 color: Color override for this specific output.202203 If `None`, uses `self.color`.204 **kwargs: Additional keyword arguments.205206 """207 self._write(action.log, color=color or self.color)208209 @override210 def on_tool_end(211 self,212 output: str,213 color: str | None = None,214 observation_prefix: str | None = None,215 llm_prefix: str | None = None,216 **kwargs: Any,217 ) -> None:218 """Handle tool end by writing the output with optional prefixes.219220 Args:221 output: The tool output to write.222 color: Color override for this specific output.223224 If `None`, uses `self.color`.225 observation_prefix: Optional prefix to write before the output.226 llm_prefix: Optional prefix to write after the output.227 **kwargs: Additional keyword arguments.228229 """230 if observation_prefix is not None:231 self._write(f"\n{observation_prefix}")232 self._write(output)233 if llm_prefix is not None:234 self._write(f"\n{llm_prefix}")235236 @override237 def on_text(238 self, text: str, color: str | None = None, end: str = "", **kwargs: Any239 ) -> None:240 """Handle text output.241242 Args:243 text: The text to write.244 color: Color override for this specific output.245246 If `None`, uses `self.color`.247 end: String appended after the text.248 **kwargs: Additional keyword arguments.249250 """251 self._write(text, color=color or self.color, end=end)252253 @override254 def on_agent_finish(255 self, finish: AgentFinish, color: str | None = None, **kwargs: Any256 ) -> None:257 """Handle agent finish by writing the finish log.258259 Args:260 finish: The agent finish object containing the log to write.261 color: Color override for this specific output.262263 If `None`, uses `self.color`.264 **kwargs: Additional keyword arguments.265266 """267 self._write(finish.log, color=color or self.color, end="\n")
Same data, no extra tab — call code_get_file + code_get_findings over MCP from Claude/Cursor/Copilot.