Team Ai
Datasetpublic

codekingpro/portable-devtools

sourceHugging Faceupdated 5mo agoView on Hugging Face
1likes14kdownloads
langchain.py491 linesDownload Raw Back to tracers
1"""A tracer implementation that records to LangChain endpoint."""2 3from __future__ import annotations4 5import logging6from concurrent.futures import ThreadPoolExecutor7from datetime import datetime, timezone8from typing import TYPE_CHECKING, Any, cast9from uuid import UUID10 11from langsmith import Client, get_tracing_context12from langsmith import run_trees as rt13from langsmith import utils as ls_utils14from tenacity import (15    Retrying,16    retry_if_exception_type,17    stop_after_attempt,18    wait_exponential_jitter,19)20from typing_extensions import override21 22from langchain_core.env import get_runtime_environment23from langchain_core.load import dumpd24from langchain_core.messages.ai import UsageMetadata, add_usage25from langchain_core.tracers._compat import run_construct, run_to_dict26from langchain_core.tracers.base import BaseTracer27from langchain_core.tracers.schemas import Run28 29if TYPE_CHECKING:30    from collections.abc import Mapping31 32    from langchain_core.messages import BaseMessage33    from langchain_core.outputs import ChatGenerationChunk, GenerationChunk34 35logger = logging.getLogger(__name__)36_LOGGED = set()37_EXECUTOR: ThreadPoolExecutor | None = None38 39OVERRIDABLE_LANGSMITH_INHERITABLE_METADATA_KEYS: frozenset[str] = frozenset(40    {"ls_agent_type"}41)42"""Allowlist of LangSmith-only tracing metadata keys that bypass the default43"first wins" merge semantics used when propagating tracer metadata to nested44runs.45 46Keys in this set are ALWAYS overridden by the nearest enclosing tracer config,47so nested callers (e.g. a subagent) can replace a value inherited from an48ancestor.49 50Keep this list very small: every key here loses the default "first wins"51protection and is always clobbered by the nearest enclosing tracer config.52Only keys that are strictly for LangSmith tracing bookkeeping should be added.53"""54 55 56def log_error_once(method: str, exception: Exception) -> None:57    """Log an error once.58 59    Args:60        method: The method that raised the exception.61        exception: The exception that was raised.62    """63    if (method, type(exception)) in _LOGGED:64        return65    _LOGGED.add((method, type(exception)))66    logger.error(exception)67 68 69def wait_for_all_tracers() -> None:70    """Wait for all tracers to finish."""71    if rt._CLIENT is not None:  # noqa: SLF00172        rt._CLIENT.flush()  # noqa: SLF00173 74 75def get_client() -> Client:76    """Get the client.77 78    Returns:79        The LangSmith client.80    """81    return rt.get_cached_client()82 83 84def _get_executor() -> ThreadPoolExecutor:85    """Get the executor."""86    global _EXECUTOR  # noqa: PLW060387    if _EXECUTOR is None:88        _EXECUTOR = ThreadPoolExecutor()89    return _EXECUTOR90 91 92def _get_usage_metadata_from_generations(93    generations: list[list[dict[str, Any]]],94) -> UsageMetadata | None:95    """Extract and aggregate `usage_metadata` from generations.96 97    Iterates through generations to find and aggregate all `usage_metadata` found in98    messages. This expects the serialized message payload shape produced by tracer99    internals:100 101        `{"message": {"kwargs": {"usage_metadata": {...}}}}`102 103    Args:104        generations: List of generation batches, where each batch is a list of105            generation dicts that may contain a `'message'` key with106            usage metadata.107 108    Returns:109        The aggregated `usage_metadata` dict if found, otherwise `None`.110    """111    output: UsageMetadata | None = None112    for generation_batch in generations:113        for generation in generation_batch:114            if isinstance(generation, dict) and "message" in generation:115                message = generation["message"]116                usage_metadata = _get_usage_metadata_from_message(message)117                if usage_metadata is not None:118                    output = add_usage(output, usage_metadata)119    return output120 121 122def _get_usage_metadata_from_message(message: Any) -> UsageMetadata | None:123    """Extract usage metadata from a generation's message payload."""124    if not isinstance(message, dict):125        return None126 127    kwargs = message.get("kwargs")128    if isinstance(kwargs, dict) and isinstance(kwargs.get("usage_metadata"), dict):129        return cast("UsageMetadata", kwargs["usage_metadata"])130 131    return None132 133 134class LangChainTracer(BaseTracer):135    """Implementation of the `SharedTracer` that `POSTS` to the LangChain endpoint."""136 137    run_inline = True138 139    def __init__(140        self,141        example_id: UUID | str | None = None,142        project_name: str | None = None,143        client: Client | None = None,144        tags: list[str] | None = None,145        *,146        metadata: Mapping[str, str] | None = None,147        **kwargs: Any,148    ) -> None:149        """Initialize the LangChain tracer.150 151        Args:152            example_id: The example ID.153            project_name: The project name.154 155                Defaults to the tracer project.156            client: The client.157 158                Defaults to the global client.159            tags: The tags.160 161                Defaults to an empty list.162            metadata: Additional metadata to include if it isn't already in the run.163 164                Defaults to None.165            **kwargs: Additional keyword arguments.166        """167        super().__init__(**kwargs)168        self.example_id = (169            UUID(example_id) if isinstance(example_id, str) else example_id170        )171        self.project_name = project_name or ls_utils.get_tracer_project()172        self.client = client or get_client()173        self.tags = tags or []174        self.latest_run: Run | None = None175        self.run_has_token_event_map: dict[str, bool] = {}176        self.tracing_metadata: dict[str, str] | None = (177            dict(metadata) if metadata is not None else None178        )179 180    def copy_with_metadata_defaults(181        self,182        *,183        metadata: Mapping[str, str] | None = None,184        tags: list[str] | None = None,185    ) -> LangChainTracer:186        """Return a new tracer with merged tracer-only defaults."""187        base_metadata = self.tracing_metadata188        if metadata is None:189            merged_metadata = dict(base_metadata) if base_metadata is not None else None190        elif base_metadata is None:191            merged_metadata = dict(metadata)192        else:193            merged_metadata = dict(base_metadata)194            for key, value in metadata.items():195                # For allowlisted LangSmith-only inheritable metadata keys196                # (e.g. ``ls_agent_type``), nested callers are allowed to197                # OVERRIDE the value inherited from an ancestor. For all198                # other keys we keep the existing "first wins" behavior so199                # that ancestor-provided tracing metadata is not accidentally200                # clobbered by child runs.201                if (202                    key not in merged_metadata203                    or key in OVERRIDABLE_LANGSMITH_INHERITABLE_METADATA_KEYS204                ):205                    merged_metadata[key] = value206 207        merged_tags = sorted(set(self.tags + tags)) if tags else self.tags208 209        return self.__class__(210            example_id=self.example_id,211            project_name=self.project_name,212            client=self.client,213            tags=merged_tags,214            metadata=merged_metadata,215            run_map=self.run_map,216            order_map=self.order_map,217            _external_run_ids=self._external_run_ids,218        )219 220    def _start_trace(self, run: Run) -> None:221        if self.project_name:222            run.session_name = self.project_name223        if self.tags is not None:224            if run.tags:225                run.tags = sorted(set(run.tags + self.tags))226            else:227                run.tags = self.tags.copy()228 229        super()._start_trace(run)230        if run.ls_client is None:231            run.ls_client = self.client232        if get_tracing_context().get("enabled") is False:233            run.extra["__disabled"] = True234 235    def on_chat_model_start(236        self,237        serialized: dict[str, Any],238        messages: list[list[BaseMessage]],239        *,240        run_id: UUID,241        tags: list[str] | None = None,242        parent_run_id: UUID | None = None,243        metadata: dict[str, Any] | None = None,244        name: str | None = None,245        **kwargs: Any,246    ) -> Run:247        """Start a trace for an LLM run.248 249        Args:250            serialized: The serialized model.251            messages: The messages.252            run_id: The run ID.253            tags: The tags.254            parent_run_id: The parent run ID.255            metadata: The metadata.256            name: The name.257            **kwargs: Additional keyword arguments.258 259        Returns:260            The run.261        """262        start_time = datetime.now(timezone.utc)263        if metadata:264            kwargs.update({"metadata": metadata})265        chat_model_run = Run(266            id=run_id,267            parent_run_id=parent_run_id,268            serialized=serialized,269            inputs={"messages": [[dumpd(msg) for msg in batch] for batch in messages]},270            extra=kwargs,271            events=[{"name": "start", "time": start_time}],272            start_time=start_time,273            run_type="llm",274            tags=tags,275            name=name,276        )277        self._start_trace(chat_model_run)278        self._on_chat_model_start(chat_model_run)279        return chat_model_run280 281    def _persist_run(self, run: Run) -> None:282        # We want to free up more memory by avoiding keeping a reference to the283        # whole nested run tree.284        run_data = run_to_dict(run, exclude={"child_runs", "inputs", "outputs"})285        self.latest_run = run_construct(286            **run_data,287            inputs=run.inputs,288            outputs=run.outputs,289        )290 291    def get_run_url(self) -> str:292        """Get the LangSmith root run URL.293 294        Returns:295            The LangSmith root run URL.296 297        Raises:298            ValueError: If no traced run is found.299            ValueError: If the run URL cannot be found.300        """301        if not self.latest_run:302            msg = "No traced run found."303            raise ValueError(msg)304        # If this is the first run in a project, the project may not yet be created.305        # This method is only really useful for debugging flows, so we will assume306        # there is some tolerace for latency.307        for attempt in Retrying(308            stop=stop_after_attempt(5),309            wait=wait_exponential_jitter(),310            retry=retry_if_exception_type(ls_utils.LangSmithError),311        ):312            with attempt:313                return self.client.get_run_url(314                    run=self.latest_run, project_name=self.project_name315                )316        msg = "Failed to get run URL."317        raise ValueError(msg)318 319    def _get_tags(self, run: Run) -> list[str]:320        """Get combined tags for a run."""321        tags = set(run.tags or [])322        tags.update(self.tags or [])323        return list(tags)324 325    def _persist_run_single(self, run: Run) -> None:326        """Persist a run."""327        if run.extra.get("__disabled"):328            return329        try:330            run.extra["runtime"] = get_runtime_environment()331            run.tags = self._get_tags(run)332            _patch_missing_metadata(self, run)333            if run.ls_client is not self.client:334                run.ls_client = self.client335            run.post()336        except Exception as e:337            # Errors are swallowed by the thread executor so we need to log them here338            log_error_once("post", e)339            raise340 341    @staticmethod342    def _update_run_single(run: Run) -> None:343        """Update a run."""344        if run.extra.get("__disabled"):345            return346        try:347            run.patch(exclude_inputs=run.extra.get("inputs_is_truthy", False))348        except Exception as e:349            # Errors are swallowed by the thread executor so we need to log them here350            log_error_once("patch", e)351            raise352 353    def _on_llm_start(self, run: Run) -> None:354        """Persist an LLM run."""355        if run.parent_run_id is None:356            run.reference_example_id = self.example_id357        self._persist_run_single(run)358 359    @override360    def _llm_run_with_token_event(361        self,362        token: str,363        run_id: UUID,364        chunk: GenerationChunk | ChatGenerationChunk | None = None,365        parent_run_id: UUID | None = None,366    ) -> Run:367        run_id_str = str(run_id)368        if run_id_str not in self.run_has_token_event_map:369            self.run_has_token_event_map[run_id_str] = True370        else:371            return self._get_run(run_id, run_type={"llm", "chat_model"})372        return super()._llm_run_with_token_event(373            # Drop the chunk; we don't need to save it374            token,375            run_id,376            chunk=None,377            parent_run_id=parent_run_id,378        )379 380    def _on_chat_model_start(self, run: Run) -> None:381        """Persist a chat model run.382 383        Note:384            Naming is historical: there is no `_on_chat_model_end` hook. Chat385            model completion is handled by `_on_llm_end`, shared with text386            LLM runs.387        """388        if run.parent_run_id is None:389            run.reference_example_id = self.example_id390        self._persist_run_single(run)391 392    def _on_llm_end(self, run: Run) -> None:393        """Process LLM/chat model run completion."""394        # Extract usage_metadata from outputs and store in extra.metadata395        if run.outputs and "generations" in run.outputs:396            usage_metadata = _get_usage_metadata_from_generations(397                run.outputs["generations"]398            )399            if usage_metadata is not None:400                if "metadata" not in run.extra:401                    run.extra["metadata"] = {}402                run.extra["metadata"]["usage_metadata"] = usage_metadata403        self._update_run_single(run)404 405    def _on_llm_error(self, run: Run) -> None:406        """Process the LLM Run upon error."""407        self._update_run_single(run)408 409    def _on_chain_start(self, run: Run) -> None:410        """Process the Chain Run upon start."""411        if run.parent_run_id is None:412            run.reference_example_id = self.example_id413        # Skip persisting if inputs are deferred (e.g., iterator/generator inputs).414        # The run will be posted when _on_chain_end is called with realized inputs.415        if not run.extra.get("defers_inputs"):416            self._persist_run_single(run)417 418    def _on_chain_end(self, run: Run) -> None:419        """Process the Chain Run."""420        # If inputs were deferred, persist (POST) the run now that inputs are realized.421        # Otherwise, update (PATCH) the existing run.422        if run.extra.get("defers_inputs"):423            self._persist_run_single(run)424        else:425            self._update_run_single(run)426 427    def _on_chain_error(self, run: Run) -> None:428        """Process the Chain Run upon error."""429        # If inputs were deferred, persist (POST) the run now that inputs are realized.430        # Otherwise, update (PATCH) the existing run.431        if run.extra.get("defers_inputs"):432            self._persist_run_single(run)433        else:434            self._update_run_single(run)435 436    def _on_tool_start(self, run: Run) -> None:437        """Process the Tool Run upon start."""438        if run.parent_run_id is None:439            run.reference_example_id = self.example_id440        self._persist_run_single(run)441 442    def _on_tool_end(self, run: Run) -> None:443        """Process the Tool Run."""444        self._update_run_single(run)445 446    def _on_tool_error(self, run: Run) -> None:447        """Process the Tool Run upon error."""448        self._update_run_single(run)449 450    def _on_retriever_start(self, run: Run) -> None:451        """Process the Retriever Run upon start."""452        if run.parent_run_id is None:453            run.reference_example_id = self.example_id454        self._persist_run_single(run)455 456    def _on_retriever_end(self, run: Run) -> None:457        """Process the Retriever Run."""458        self._update_run_single(run)459 460    def _on_retriever_error(self, run: Run) -> None:461        """Process the Retriever Run upon error."""462        self._update_run_single(run)463 464    def wait_for_futures(self) -> None:465        """Wait for the given futures to complete."""466        if self.client is not None:467            self.client.flush()468 469 470def _patch_missing_metadata(self: LangChainTracer, run: Run) -> None:471    if not self.tracing_metadata:472        return473    metadata = run.metadata474    patched = None475    for k, v in self.tracing_metadata.items():476        # ``OVERRIDABLE_LANGSMITH_INHERITABLE_METADATA_KEYS`` are a small,477        # LangSmith-only allowlist that bypasses the "first wins" merge478        # so a nested caller (e.g. a subagent) can override a parent-set value.479        if k not in metadata or k in OVERRIDABLE_LANGSMITH_INHERITABLE_METADATA_KEYS:480            # Skip the copy when the value already matches (avoids cloning481            # the shared dict in the common "already set" case). Use a482            # ``k in metadata`` guard so a legitimate missing key whose483            # tracer value happens to be ``None`` is still patched in.484            if k in metadata and metadata[k] == v:485                continue486            if patched is None:487                # Copy on first miss to avoid mutating the shared dict.488                patched = {**metadata}489                run.extra["metadata"] = patched490            patched[k] = v491 
codekingpro/portable-devtools · Team Ai