codekingpro/portable-devtools
114k
1"""Client for interacting with the LangSmith API.2 3Use the client to customize API keys / workspace connections, SSL certs,4etc. for tracing.5 6Also used to create, read, update, and delete LangSmith resources7such as runs (~trace spans), datasets, examples (~records),8feedback (~metrics), projects (tracer sessions/groups), etc.9 10For detailed API documentation, visit the [LangSmith docs](https://docs.langchain.com/langsmith/home).11"""12 13from __future__ import annotations14 15import atexit16import base6417import collections18import concurrent.futures as cf19import contextlib20import datetime21import functools22import importlib23import importlib.metadata24import io25import itertools26import json27import logging28import os29import random30import threading31import time32import traceback33import typing34import uuid35import warnings36import weakref37from collections.abc import AsyncIterable, Iterable, Iterator, Mapping, Sequence38from functools import lru_cache, partial39from inspect import signature40from pathlib import Path41from queue import Full, PriorityQueue42from typing import (43 TYPE_CHECKING,44 Annotated,45 Any,46 Callable,47 Literal,48 Optional,49 TypedDict,50 Union,51 cast,52 get_args,53)54from urllib import parse as urllib_parse55 56import packaging.version57import requests58from pydantic import Field59from requests import adapters as requests_adapters60from requests_toolbelt import ( # type: ignore[import-untyped]61 multipart as rqtb_multipart,62)63from typing_extensions import TypeGuard, overload64from urllib3.poolmanager import PoolKey # type: ignore[attr-defined, import-untyped]65from urllib3.util import Retry # type: ignore[import-untyped]66 67import langsmith68from langsmith import env as ls_env69from langsmith import schemas as ls_schemas70from langsmith import utils as ls_utils71from langsmith._internal import _orjson, _profiles72from langsmith._internal._background_thread import (73 TracingQueueItem,74)75from langsmith._internal._background_thread import (76 tracing_control_thread_func as _tracing_control_thread_func,77)78from langsmith._internal._beta_decorator import warn_beta79from langsmith._internal._compressed_traces import CompressedTraces80from langsmith._internal._constants import (81 _AUTO_SCALE_UP_NTHREADS_LIMIT,82 _BLOCKSIZE_BYTES,83 _BOUNDARY,84 _SIZE_LIMIT_BYTES,85 _TRACING_QUEUE_MAX_SIZE,86)87from langsmith._internal._hub import (88 HUB,89 PLATFORM_HUB,90 REPO_HANDLE_PATTERN,91 build_commit_url,92 resolve_owner_for_url,93 validate_parent_commit,94)95from langsmith._internal._multipart import (96 MultipartPart,97 MultipartPartsAndContext,98 join_multipart_parts_and_context,99)100from langsmith._internal._operations import (101 SerializedFeedbackOperation,102 SerializedRunOperation,103 combine_serialized_queue_operations,104 compress_multipart_parts_and_context,105 serialize_feedback_dict,106 serialize_run_dict,107 serialized_feedback_operation_to_multipart_parts_and_context,108 serialized_run_operation_to_multipart_parts_and_context,109)110from langsmith._internal._serde import dumps_json as _dumps_json111from langsmith._internal._uuid import uuid7112from langsmith.prompt_cache import PromptCache, prompt_cache_singleton113from langsmith.schemas import AttachmentInfo, ExampleWithRuns114 115logger = logging.getLogger(__name__)116 117_TRACING_DROP_LOG_INTERVAL_S = 60118_tracing_drops_count = 0119_tracing_drops_last_log_time = 0.0120_tracing_drops_lock = threading.Lock()121TracingMode = Literal["langsmith", "otel", "hybrid"]122_VALID_TRACING_MODES: frozenset[str] = frozenset(get_args(TracingMode))123 124 125def _log_tracing_drop(reason: str) -> None:126 """Rate-limited logging for dropped trace data (once per 60s)."""127 global _tracing_drops_count, _tracing_drops_last_log_time128 with _tracing_drops_lock:129 _tracing_drops_count += 1130 now = time.time()131 if now - _tracing_drops_last_log_time >= _TRACING_DROP_LOG_INTERVAL_S:132 count = _tracing_drops_count133 _tracing_drops_count = 0134 _tracing_drops_last_log_time = now135 logger.warning(136 "Dropped %d trace data item(s) in the last %ds: %s",137 count,138 _TRACING_DROP_LOG_INTERVAL_S,139 reason,140 )141 142 143def _reset_tracing_drop_log() -> None:144 """Reset rate-limit state for drop logging. Used in tests."""145 global _tracing_drops_count, _tracing_drops_last_log_time146 with _tracing_drops_lock:147 _tracing_drops_count = 0148 _tracing_drops_last_log_time = 0.0149 150 151_TRACING_SEND_TIMEOUT = (3, 10) # (connect, read) seconds for background sends152 153_OPENAI_API_KEY = "OPENAI_API_KEY"154_ANTHROPIC_API_KEY = "ANTHROPIC_API_KEY"155 156 157def _resolve_tracing_mode(158 tracing_mode: Optional[TracingMode],159 *,160 otel_enabled: Optional[bool] = None,161) -> TracingMode:162 """Resolve the effective tracing mode from the constructor arg and env vars.163 164 Priority: explicit ``tracing_mode`` argument >165 deprecated ``otel_enabled`` argument >166 ``LANGSMITH_TRACING_MODE`` env var >167 legacy ``OTEL_ENABLED`` / ``OTEL_ONLY`` env vars >168 default ``"langsmith"``.169 """170 mode_envvar_name = "TRACING_MODE"171 otel_enabled_envvar_name = "OTEL_ENABLED"172 otel_only_envvar_name = "OTEL_ONLY"173 174 env_mode = ls_utils.get_env_var(mode_envvar_name)175 176 if tracing_mode is not None:177 tracing_mode = tracing_mode.lower() # type: ignore[assignment]178 if tracing_mode not in _VALID_TRACING_MODES:179 raise ls_utils.LangSmithUserError(180 f"Invalid tracing_mode={tracing_mode!r}. "181 f"Must be one of: {', '.join(sorted(_VALID_TRACING_MODES))}"182 )183 return tracing_mode # type: ignore[return-value]184 185 if otel_enabled is not None:186 warnings.warn(187 "The 'otel_enabled' parameter is deprecated and will be removed "188 "in the next minor version. Use 'tracing_mode' instead, e.g. "189 'Client(tracing_mode="hybrid") or Client(tracing_mode="otel").',190 FutureWarning,191 stacklevel=3,192 )193 if otel_enabled:194 if ls_utils.is_env_var_truish(otel_only_envvar_name):195 return "otel"196 return "hybrid"197 return "langsmith"198 199 if env_mode is not None:200 env_mode = env_mode.lower()201 if env_mode not in _VALID_TRACING_MODES:202 raise ls_utils.LangSmithUserError(203 f"Invalid LANGSMITH_TRACING_MODE={env_mode!r}. "204 f"Must be one of: {', '.join(sorted(_VALID_TRACING_MODES))}"205 )206 legacy_otel = ls_utils.is_env_var_truish(otel_enabled_envvar_name)207 legacy_only = ls_utils.is_env_var_truish(otel_only_envvar_name)208 if legacy_otel or legacy_only:209 warnings.warn(210 f"Both LANGSMITH_{mode_envvar_name} and the legacy "211 f"LANGSMITH_{otel_enabled_envvar_name} / "212 f"LANGSMITH_{otel_only_envvar_name} env vars are set. "213 f"LANGSMITH_{mode_envvar_name} takes precedence.",214 stacklevel=3,215 )216 return env_mode # type: ignore[return-value]217 218 # Fall back to legacy env vars219 if ls_utils.is_env_var_truish(otel_only_envvar_name):220 return "otel"221 if ls_utils.is_env_var_truish("OTEL_ENABLED"):222 return "hybrid"223 return "langsmith"224 225 226def _import_otel():227 """Dynamically import OTEL modules when needed."""228 try:229 from opentelemetry import trace as otel_trace # type: ignore[import]230 from opentelemetry.trace import set_span_in_context # type: ignore[import]231 232 from langsmith._internal.otel._otel_client import (233 get_otlp_tracer_provider,234 )235 from langsmith._internal.otel._otel_exporter import OTELExporter236 237 return otel_trace, set_span_in_context, get_otlp_tracer_provider, OTELExporter238 except ImportError:239 raise ImportError(240 "To use OTEL tracing, you must install it with `pip install langsmith[otel]`"241 )242 243 244try:245 from zoneinfo import ZoneInfo # type: ignore[import-not-found]246except ImportError:247 248 class ZoneInfo: # type: ignore[no-redef]249 """Introduced in python 3.9."""250 251 252try:253 from opentelemetry.sdk.trace import TracerProvider # type: ignore[import-not-found]254except ImportError:255 256 class TracerProvider: # type: ignore[no-redef]257 """Used for optional OTEL tracing."""258 259 260if TYPE_CHECKING:261 import pandas as pd # type: ignore262 from langchain_core.runnables import Runnable263 264 from langsmith import schemas265 266 # OTEL imports for type hints267 try:268 from opentelemetry import trace as otel_trace # type: ignore[import]269 270 from langsmith._internal.otel._otel_exporter import OTELExporter271 except ImportError:272 otel_trace = Any # type: ignore[assignment, misc]273 OTELExporter = Any # type: ignore[assignment, misc]274 from langsmith.evaluation import evaluator as ls_evaluator275 from langsmith.evaluation._arunner import (276 AEVALUATOR_T,277 ATARGET_T,278 AsyncExperimentResults,279 )280 from langsmith.evaluation._runner import (281 COMPARATIVE_EVALUATOR_T,282 DATA_T,283 EVALUATOR_T,284 EXPERIMENT_T,285 SUMMARY_EVALUATOR_T,286 TARGET_T,287 ComparativeExperimentResults,288 ExperimentResults,289 )290 291 292logger = logging.getLogger(__name__)293_urllib3_logger = logging.getLogger("urllib3.connectionpool")294 295X_API_KEY = "x-api-key"296EMPTY_SEQ: tuple[dict, ...] = ()297_UNSET = object()298URLLIB3_SUPPORTS_BLOCKSIZE = "key_blocksize" in signature(PoolKey).parameters299DEFAULT_INSTRUCTIONS = "How are people using my agent? What are they asking about?"300 301_fallback_dirs_created: set[str] = set()302 303 304@lru_cache(maxsize=1)305def _lc_load_allowed_objects_arg_supported() -> bool:306 """Check if the installed `langchain_core.load.load` supports the `allowed_objects` parameter.307 308 Returns `True` if `langchain-core >= 0.3.81` and `< 1.0`, or `>= 1.2.5`.309 """310 allowed_objects_supported = False311 try:312 from langchain_core import __version__313 314 lc_version = packaging.version.parse(__version__)315 # allowed_objects supported in langchain-core >= 0.3.81 and < 1.0, or >= 1.2.5316 allowed_objects_supported = (317 lc_version >= packaging.version.parse("0.3.81")318 and lc_version < packaging.version.parse("1.0.0")319 ) or (lc_version >= packaging.version.parse("1.2.5"))320 except (ImportError, ValueError, TypeError) as exc:321 # If version checking fails, default to False322 logger.debug(323 "Failed to determine langchain-core version for allowed_objects "324 "support, defaulting to disabled: %s",325 exc,326 )327 return allowed_objects_supported328 329 330def _manifest_has_secrets(331 manifest: dict | list, *, depth: int = 0, max_depth: int = 10, max_width: int = 50332) -> bool:333 """Recursively check if a manifest contains any secret objects."""334 if max_depth < 1:335 raise ValueError("max_depth must be positive.")336 if max_width < 1:337 raise ValueError("max_width must be positive.")338 if depth >= max_depth:339 return False340 if (341 isinstance(manifest, dict)342 and set(manifest) == {"lc", "type", "id"}343 and manifest["type"] == "secret"344 ):345 return True346 elif depth + 1 == max_depth: # skip extra layer of function calls.347 return False348 elif isinstance(manifest, dict):349 return any(350 _manifest_has_secrets(351 x, depth=depth + 1, max_depth=max_depth, max_width=max_width352 )353 for x in itertools.islice(manifest.values(), max_width)354 )355 elif isinstance(manifest, (tuple, list)):356 return any(357 _manifest_has_secrets(358 x, depth=depth + 1, max_depth=max_depth, max_width=max_width359 )360 for x in manifest[:max_width]361 )362 else:363 return False364 365 366def _validate_public_prompt_pull(367 prompt_identifier: str, *, dangerously_pull_public_prompt: bool368) -> None:369 owner, _, _ = ls_utils.parse_prompt_identifier(prompt_identifier)370 if owner != "-" and not dangerously_pull_public_prompt:371 raise ValueError(372 "Pulling a public prompt by owner/name is disabled by default because "373 "prompts may contain untrusted serialized LangChain objects. If you "374 "trust this prompt, set `dangerously_pull_public_prompt=True` to "375 "acknowledge the risk."376 )377 378 379def _process_prompt_manifest(380 prompt_object: Any,381 *,382 include_model: bool | None,383 secrets: dict[str, str] | None,384 secrets_from_env: bool,385) -> Any:386 """Process a prompt manifest into a LangChain prompt object.387 388 This is the common logic shared between `Client.pull_prompt()` and389 `AsyncClient.pull_prompt()`.390 391 Args:392 prompt_object: The prompt commit object containing the manifest.393 include_model: Whether to include model information.394 secrets: Map of secrets to use when loading.395 secrets_from_env: Whether to load secrets from environment variables.396 397 Returns:398 The processed prompt object.399 400 Raises:401 ImportError: If `langchain-core` is not installed.402 ValueError: If secrets are required but not provided.403 """404 try:405 from langchain_core.language_models.base import BaseLanguageModel406 from langchain_core.load.load import load407 from langchain_core.output_parsers import BaseOutputParser408 from langchain_core.prompts import BasePromptTemplate409 from langchain_core.prompts.structured import StructuredPrompt410 from langchain_core.runnables.base import RunnableBinding, RunnableSequence411 except ImportError:412 raise ImportError(413 "The client.pull_prompt function requires the langchain-core "414 "package to run.\nInstall with `pip install langchain-core`"415 )416 try:417 from langchain_core._api import suppress_langchain_beta_warning418 except ImportError:419 420 @contextlib.contextmanager421 def suppress_langchain_beta_warning():422 yield423 424 load_kwargs: dict = {}425 if _lc_load_allowed_objects_arg_supported():426 load_kwargs["allowed_objects"] = "all" if include_model else "core"427 428 with suppress_langchain_beta_warning():429 try:430 prompt = load(431 prompt_object.manifest,432 secrets_map=secrets,433 secrets_from_env=secrets_from_env,434 **load_kwargs,435 )436 except Exception as e:437 if (438 _manifest_has_secrets(prompt_object.manifest)439 and not secrets_from_env440 and not secrets441 ):442 raise ValueError(443 "Failed to load prompt. The prompt manifest contains secrets "444 "(like API keys or access tokens) but no secrets were provided. "445 "This is due to a security patch in langsmith 0.5.1 that "446 "disabled reading secrets from environment variables by default.\n\n"447 "To resolve this:\n"448 "- Recommended: Pass secrets directly via `secrets={'KEY_NAME': 'value'}`\n"449 "- If this is a trusted prompt: Set `secrets_from_env=True` to read "450 "secrets from environment variables\n\n"451 f"Underlying error:\n{e}"452 )453 raise e454 455 if (456 isinstance(prompt, BasePromptTemplate)457 or isinstance(prompt, RunnableSequence)458 and isinstance(prompt.first, BasePromptTemplate)459 ):460 prompt_template = (461 prompt462 if isinstance(prompt, BasePromptTemplate)463 else (464 prompt.first465 if isinstance(prompt, RunnableSequence)466 and isinstance(prompt.first, BasePromptTemplate)467 else None468 )469 )470 if prompt_template is None:471 raise ls_utils.LangSmithError(472 "Prompt object is not a valid prompt template."473 )474 475 if prompt_template.metadata is None:476 prompt_template.metadata = {}477 prompt_template.metadata.update(478 {479 "lc_hub_owner": prompt_object.owner,480 "lc_hub_repo": prompt_object.repo,481 "lc_hub_commit_hash": prompt_object.commit_hash,482 }483 )484 485 # Transform 2-step RunnableSequence to 3-step for structured prompts486 # See create_commit for the reverse transformation487 if (488 include_model489 and isinstance(prompt, RunnableSequence)490 and isinstance(prompt.first, StructuredPrompt)491 # Make forward-compatible in case we let update the response type492 and (len(prompt.steps) == 2 and not isinstance(prompt.last, BaseOutputParser))493 ):494 if isinstance(prompt.last, RunnableBinding) and isinstance(495 prompt.last.bound, BaseLanguageModel496 ):497 seq = cast(RunnableSequence, prompt.first | prompt.last.bound)498 if len(seq.steps) == 3: # prompt | bound llm | output parser499 rebound_llm = seq.steps[1]500 prompt = RunnableSequence(501 prompt.first,502 rebound_llm.bind(**{**prompt.last.kwargs}),503 seq.last,504 )505 else:506 prompt = seq # Not sure507 508 elif isinstance(prompt.last, BaseLanguageModel):509 prompt: RunnableSequence = prompt.first | prompt.last # type: ignore[no-redef, assignment]510 else:511 pass512 513 return prompt514 515 516def _parse_token_or_url(517 url_or_token: Union[str, uuid.UUID],518 api_url: str,519 num_parts: int = 2,520 kind: str = "dataset",521) -> tuple[str, str]:522 """Parse a public dataset URL or share token."""523 try:524 if isinstance(url_or_token, uuid.UUID) or uuid.UUID(url_or_token):525 return api_url, str(url_or_token)526 except ValueError:527 pass528 529 # Then it's a URL530 parsed_url = urllib_parse.urlparse(str(url_or_token))531 # Extract the UUID from the path532 path_parts = parsed_url.path.split("/")533 if len(path_parts) >= num_parts:534 token_uuid = path_parts[-num_parts]535 _as_uuid(token_uuid, var="token parts")536 else:537 raise ls_utils.LangSmithUserError(f"Invalid public {kind} URL: {url_or_token}")538 if parsed_url.netloc == "smith.langchain.com":539 api_url = "https://api.smith.langchain.com"540 elif parsed_url.netloc == "beta.smith.langchain.com":541 api_url = "https://beta.api.smith.langchain.com"542 return api_url, token_uuid543 544 545def _is_langchain_hosted(url: str) -> bool:546 """Check if the URL is langchain hosted.547 548 Args:549 url: The URL to check.550 551 Returns:552 `True` if the URL is langchain hosted, `False` otherwise.553 """554 try:555 netloc = urllib_parse.urlsplit(url).netloc.split(":")[0]556 return netloc == "langchain.com" or netloc.endswith(".langchain.com")557 except Exception:558 return False559 560 561ID_TYPE = Union[uuid.UUID, str]562RUN_TYPE_T = Literal[563 "tool", "chain", "llm", "retriever", "embedding", "prompt", "parser"564]565 566 567@functools.lru_cache(maxsize=1)568def _default_retry_config() -> Retry:569 """Get the default retry configuration.570 571 If `urllib3` version is `1.26` or greater, retry on all methods.572 573 Returns:574 The default retry configuration.575 """576 retry_params = dict(577 total=3,578 status_forcelist=[502, 503, 504, 408, 425],579 backoff_factor=0.5,580 # Sadly urllib3 1.x doesn't support backoff_jitter581 raise_on_redirect=False,582 raise_on_status=False,583 respect_retry_after_header=True,584 )585 586 # the `allowed_methods` keyword is not available in urllib3 < 1.26587 588 # check to see if urllib3 version is 1.26 or greater589 urllib3_version = packaging.version.parse(importlib.metadata.version("urllib3"))590 use_allowed_methods = urllib3_version >= packaging.version.parse("1.26")591 592 if use_allowed_methods:593 # Retry on all methods594 retry_params["allowed_methods"] = None595 596 return ls_utils.LangSmithRetry(**retry_params) # type: ignore597 598 599def close_session(session: requests.Session) -> None:600 """Close the session.601 602 Args:603 session: The session to close.604 """605 logger.debug("Closing Client.session")606 session.close()607 608 609def _get_langsmith_env_var_uncached(name: str) -> Optional[str]:610 for namespace in ("LANGSMITH", "LANGCHAIN"):611 value = os.environ.get(f"{namespace}_{name}")612 if value is not None and value.strip() != "":613 return value614 return None615 616 617def _validate_api_key_if_hosted(618 api_url: str,619 api_key: Optional[str],620 *,621 tracing_mode: TracingMode = "langsmith",622) -> None:623 """Verify API key is provided if url not localhost.624 625 Args:626 api_url: The API URL.627 api_key: The API key.628 tracing_mode: Resolved tracing mode; when ``"otel"`` the warning is629 suppressed because no LangSmith REST calls are made.630 631 Raises:632 LangSmithUserError: If the API key is not provided when using the hosted service.633 """634 if not api_key:635 if (636 _is_langchain_hosted(api_url)637 and tracing_mode != "otel"638 and ls_utils.tracing_is_enabled()639 ):640 warnings.warn(641 "API key must be provided when using hosted LangSmith API",642 ls_utils.LangSmithMissingAPIKeyWarning,643 )644 645 646def _format_feedback_score(score: Union[float, int, bool, None]):647 """Format a feedback score by truncating numerical values to 4 decimal places.648 649 Args:650 score: The score to format, can be a number or any other type651 652 Returns:653 The formatted score654 """655 if isinstance(score, float):656 # Truncate at 4 decimal places657 return round(score, 4)658 return score659 660 661def _get_tracing_sampling_rate(662 tracing_sampling_rate: Optional[float] = None,663) -> float | None:664 """Get the tracing sampling rate.665 666 Returns:667 The tracing sampling rate.668 """669 if tracing_sampling_rate is None:670 sampling_rate_str = ls_utils.get_env_var("TRACING_SAMPLING_RATE")671 if not sampling_rate_str:672 return None673 else:674 sampling_rate_str = str(tracing_sampling_rate)675 sampling_rate = float(sampling_rate_str)676 if sampling_rate < 0 or sampling_rate > 1:677 raise ls_utils.LangSmithUserError(678 "LANGSMITH_TRACING_SAMPLING_RATE must be between 0 and 1 if set."679 f" Got: {sampling_rate}"680 )681 return sampling_rate682 683 684def _get_write_api_urls(_write_api_urls: Optional[dict[str, str]]) -> dict[str, str]:685 # Note: LANGSMITH_RUNS_ENDPOINTS is now handled via replicas, not _write_api_urls686 _write_api_urls = _write_api_urls or {}687 processed_write_api_urls = {}688 for url, api_key in _write_api_urls.items():689 processed_url = url.strip()690 if not processed_url:691 raise ls_utils.LangSmithUserError("LangSmith runs API URL cannot be empty")692 processed_url = processed_url.strip().strip('"').strip("'").rstrip("/")693 processed_api_key = api_key.strip().strip('"').strip("'")694 _validate_api_key_if_hosted(processed_url, processed_api_key)695 processed_write_api_urls[processed_url] = processed_api_key696 697 return processed_write_api_urls698 699 700def _as_uuid(value: ID_TYPE, var: Optional[str] = None) -> uuid.UUID:701 try:702 return uuid.UUID(value) if not isinstance(value, uuid.UUID) else value703 except ValueError as e:704 var = var or "value"705 raise ls_utils.LangSmithUserError(706 f"{var} must be a valid UUID or UUID string. Got {value}"707 ) from e708 709 710@typing.overload711def _ensure_uuid(value: Optional[Union[str, uuid.UUID]]) -> uuid.UUID: ...712 713 714@typing.overload715def _ensure_uuid(716 value: Optional[Union[str, uuid.UUID]], *, accept_null: bool = True717) -> Optional[uuid.UUID]: ...718 719 720def _ensure_uuid(value: Optional[Union[str, uuid.UUID]], *, accept_null: bool = False):721 if value is None:722 if accept_null:723 return None724 return uuid7()725 return _as_uuid(value)726 727 728@functools.lru_cache(maxsize=1)729def _parse_url(url):730 parsed_url = urllib_parse.urlparse(url)731 host = parsed_url.netloc.split(":")[0]732 return host733 734 735class _LangSmithHttpAdapter(requests_adapters.HTTPAdapter):736 __attrs__ = [737 "max_retries",738 "config",739 "_pool_connections",740 "_pool_maxsize",741 "_pool_block",742 "_blocksize",743 ]744 745 def __init__(746 self,747 pool_connections: int = requests_adapters.DEFAULT_POOLSIZE,748 pool_maxsize: int = requests_adapters.DEFAULT_POOLSIZE,749 max_retries: Union[Retry, int, None] = requests_adapters.DEFAULT_RETRIES,750 pool_block: bool = requests_adapters.DEFAULT_POOLBLOCK,751 blocksize: int = 16384, # default from urllib3.BaseHTTPSConnection752 ) -> None:753 self._blocksize = blocksize754 super().__init__(pool_connections, pool_maxsize, max_retries, pool_block)755 756 def init_poolmanager(self, connections, maxsize, block=False, **pool_kwargs):757 if URLLIB3_SUPPORTS_BLOCKSIZE:758 # urllib3 before 2.0 doesn't support blocksize759 pool_kwargs["blocksize"] = self._blocksize760 try:761 return super().init_poolmanager(connections, maxsize, block, **pool_kwargs)762 except TypeError:763 if "blocksize" in pool_kwargs:764 logger.warning(765 "An intermediate HTTPAdapter does not accept the 'blocksize' "766 "kwarg. Retrying without it."767 )768 pool_kwargs.pop("blocksize")769 return super().init_poolmanager(770 connections, maxsize, block, **pool_kwargs771 )772 raise773 774 775class ListThreadsItem(TypedDict):776 """Item returned by :meth:`Client.list_threads`."""777 778 thread_id: str779 runs: list[ls_schemas.Run]780 count: int781 min_start_time: Optional[str]782 max_start_time: Optional[str]783 784 785class Client:786 """Client for interacting with the LangSmith API."""787 788 __slots__ = [789 "__weakref__",790 "api_url",791 "_api_key",792 "_oauth_access_token",793 "_workspace_id",794 "_headers",795 "_custom_headers",796 "retry_config",797 "timeout_ms",798 "_timeout",799 "session",800 "_get_data_type_cached",801 "_web_url",802 "_tenant_id",803 "tracing_sample_rate",804 "_filtered_post_uuids",805 "tracing_queue",806 "_anonymizer",807 "_hide_inputs",808 "_hide_outputs",809 "_hide_metadata",810 "_omit_traced_runtime_info",811 "_process_buffered_run_ops",812 "_run_ops_buffer_size",813 "_run_ops_buffer_timeout_ms",814 "_run_ops_buffer_last_flush_time",815 "_info",816 "_write_api_urls",817 "_settings",818 "_manual_cleanup",819 "_pyo3_client",820 "compressed_traces",821 "_data_available_event",822 "_futures",823 "_run_ops_buffer",824 "_run_ops_buffer_lock",825 "otel_exporter",826 "_otel_trace",827 "_set_span_in_context",828 "_max_batch_size_bytes",829 "_use_daemon_threads",830 "_tracing_error_callback",831 "_multipart_disabled",832 "_cache",833 "_failed_traces_dir",834 "_failed_traces_max_bytes",835 "_tracing_mode",836 "_profile_auth",837 "_profile_auth_headers",838 ]839 840 _api_key: Optional[str]841 _oauth_access_token: Optional[str]842 _headers: dict[str, str]843 _custom_headers: dict[str, str]844 _timeout: tuple[float, float]845 _manual_cleanup: bool846 _profile_auth: Optional[_profiles.ProfileAuth]847 _profile_auth_headers: dict[str, str]848 849 def __init__(850 self,851 api_url: Optional[str] = None,852 *,853 api_key: Optional[str] = None,854 retry_config: Optional[Retry] = None,855 timeout_ms: Optional[Union[int, tuple[int, int]]] = None,856 web_url: Optional[str] = None,857 session: Optional[requests.Session] = None,858 auto_batch_tracing: bool = True,859 anonymizer: Optional[Callable[[dict], dict]] = None,860 hide_inputs: Optional[Union[Callable[[dict], dict], bool]] = None,861 hide_outputs: Optional[Union[Callable[[dict], dict], bool]] = None,862 hide_metadata: Optional[Union[Callable[[dict], dict], bool]] = None,863 omit_traced_runtime_info: bool = False,864 process_buffered_run_ops: Optional[865 Callable[[Sequence[dict]], Sequence[dict]]866 ] = None,867 run_ops_buffer_size: Optional[int] = None,868 run_ops_buffer_timeout_ms: Optional[float] = None,869 info: Optional[Union[dict, ls_schemas.LangSmithInfo]] = None,870 api_urls: Optional[dict[str, str]] = None,871 otel_tracer_provider: Optional[TracerProvider] = None,872 tracing_mode: Optional[TracingMode] = None,873 otel_enabled: Optional[bool] = None,874 tracing_sampling_rate: Optional[float] = None,875 workspace_id: Optional[str] = None,876 max_batch_size_bytes: Optional[int] = None,877 headers: Optional[dict[str, str]] = None,878 tracing_error_callback: Optional[Callable[[Exception], None]] = None,879 disable_prompt_cache: bool = False,880 cache: Optional[Union[bool, PromptCache]] = None,881 ) -> None:882 """Initialize a `Client` instance.883 884 Args:885 api_url: URL for the LangSmith API.886 887 Defaults to the `LANGCHAIN_ENDPOINT` environment variable or888 `https://api.smith.langchain.com` if not set.889 api_key: API key for the LangSmith API.890 891 Defaults to the `LANGCHAIN_API_KEY` environment variable.892 retry_config: Retry configuration for the `HTTPAdapter`.893 timeout_ms: Timeout for the `HTTPAdapter`.894 895 Can also be a 2-tuple of `(connect timeout, read timeout)` to set them896 separately.897 web_url: URL for the LangSmith web app.898 899 Default is auto-inferred from the `ENDPOINT`.900 session: The session to use for requests.901 902 If `None`, a new session will be created.903 auto_batch_tracing: Whether to automatically batch tracing.904 anonymizer: A function applied for masking serialized run inputs and905 outputs, before sending to the API.906 hide_inputs: Whether to hide run inputs when tracing with this client.907 908 If `True`, hides the entire inputs.909 910 If a function, applied to all run inputs when creating runs.911 hide_outputs: Whether to hide run outputs when tracing with this client.912 913 If `True`, hides the entire outputs.914 915 If a function, applied to all run outputs when creating runs.916 hide_metadata: Whether to hide run metadata when tracing with this client.917 918 If `True`, hides the entire metadata.919 920 If a function, applied to all run metadata when creating runs.921 omit_traced_runtime_info: Whether to omit runtime information from traced922 runs.923 924 If `True`, runtime information (SDK version, platform, Python version,925 etc.) will not be stored in the `extra.runtime` field of runs.926 927 Defaults to `False`.928 process_buffered_run_ops: A function applied to buffered run operations that929 allows for modification of the raw run dicts before they are converted930 to multipart and compressed.931 932 Useful specifically for high throughput tracing where you need to apply933 a rate-limited API or other costly process to the runs before they are934 sent to the API.935 936 Note that the buffer will only flush automatically when937 `run_ops_buffer_size` is reached or a new run is added to the buffer938 after `run_ops_buffer_timeout_ms` has elapsed - it will not flush939 outside of these conditions unless you manually call `client.flush()`,940 so be sure to do this before your code exits.941 run_ops_buffer_size: Maximum number of run operations to collect in the942 buffer before applying `process_buffered_run_ops` and sending to the943 API.944 945 Required when `process_buffered_run_ops` is provided.946 run_ops_buffer_timeout_ms: Maximum time in milliseconds to wait before947 flushing the run ops buffer when new runs are added.948 949 Defaults to `5000`.950 951 Only used when `process_buffered_run_ops` is provided.952 info: The information about the LangSmith API.953 954 If not provided, it will be fetched from the API.955 api_urls: A dictionary of write API URLs and their corresponding API keys.956 957 Useful for multi-tenant setups.958 959 Data is only read from the first URL in the dictionary. However, ONLY960 Runs are written (`POST` and `PATCH`) to all URLs in the dictionary.961 Feedback, sessions, datasets, examples, annotation queues and evaluation962 results are only written to the first.963 otel_tracer_provider: Optional tracer provider for OpenTelemetry964 integration.965 966 If not provided, a LangSmith-specific tracer provider will be used.967 tracing_mode: Where to send traces. One of:968 969 - ``"langsmith"`` (default) — LangSmith only.970 - ``"otel"`` — OpenTelemetry export only.971 - ``"hybrid"`` — both OTel and LangSmith.972 973 Falls back to the ``LANGSMITH_TRACING_MODE`` env var, then to974 the legacy ``OTEL_ENABLED`` / ``OTEL_ONLY`` env vars, then to975 ``"langsmith"``.976 otel_enabled: *Deprecated.* Use ``tracing_mode`` instead.977 978 When ``True``, interpreted as ``tracing_mode="hybrid"``979 (or ``"otel"`` if the ``OTEL_ONLY`` env var is set).980 Will be removed in the next minor version.981 tracing_sampling_rate: The sampling rate for tracing.982 983 If provided, overrides the `LANGCHAIN_TRACING_SAMPLING_RATE` environment984 variable.985 986 Should be a float between `0` and `1`, where `1` means trace everything987 and `0` means trace nothing.988 workspace_id: The workspace ID.989 990 Required for org-scoped API keys.991 max_batch_size_bytes: The maximum size of a batch of runs in bytes.992 993 If not provided, the default is set by the server.994 headers: Additional HTTP headers to include in all requests.995 996 These headers will be merged with the default headers (User-Agent,997 Accept, x-api-key, etc.). Custom headers will not override the default998 required headers.999 tracing_error_callback (Optional[Callable[[Exception], None]]): Optional callback function to handle errors.1000 1001 Called when exceptions occur during tracing operations.1002 disable_prompt_cache: Disable prompt caching for this client.1003 1004 By default, prompt caching is enabled globally using a singleton cache.1005 Set this to `True` to disable caching for this specific client instance.1006 1007 To configure the global cache, use `configure_global_prompt_cache()`.1008 1009 !!! example1010 1011 ```python1012 from langsmith import Client, configure_global_prompt_cache1013 1014 # Use default global cache1015 client = Client()1016 1017 # Disable caching for this client1018 client_no_cache = Client(disable_prompt_cache=True)1019 1020 # Configure global cache settings1021 configure_global_prompt_cache(max_size=200, ttl_seconds=7200)1022 ```1023 cache: **[Deprecated]** Control prompt caching behavior.1024 1025 This parameter is deprecated. Use `configure_global_prompt_cache()` to1026 configure caching, or `disable_prompt_cache=True` to disable it.1027 1028 - `True`: Enable caching with the global singleton (default behavior)1029 - `False`: Disable caching (equivalent to `disable_prompt_cache=True`)1030 - `Cache(...)`/`PromptCache(...)`: Use a custom cache instance1031 1032 !!! example1033 1034 ```python1035 from langsmith import Client, Cache, configure_global_prompt_cache1036 1037 # Old API (deprecated but still supported)1038 client = Client(cache=True) # Use global cache1039 client = Client(cache=False) # Disable cache1040 1041 # Use custom cache instance1042 my_cache = Cache(max_size=100, ttl_seconds=3600)1043 client = Client(cache=my_cache)1044 1045 # New API (recommended)1046 client = Client() # Use global cache (default)1047 1048 # Configure global cache for all clients1049 configure_global_prompt_cache(max_size=200, ttl_seconds=7200)1050 1051 # Or disable for a specific client1052 client = Client(disable_prompt_cache=True)1053 ```1054 1055 Raises:1056 LangSmithUserError: If the API key is not provided when using the hosted service.1057 LangSmithUserError: If both `api_url` and `api_urls` are provided.1058 """1059 if api_url and api_urls:1060 raise ls_utils.LangSmithUserError(1061 "You cannot provide both api_url and api_urls."1062 )1063 1064 if (1065 os.getenv("LANGSMITH_ENDPOINT") or os.getenv("LANGCHAIN_ENDPOINT")1066 ) and os.getenv("LANGSMITH_RUNS_ENDPOINTS"):1067 raise ls_utils.LangSmithUserError(1068 "You cannot provide both LANGSMITH_ENDPOINT / LANGCHAIN_ENDPOINT "1069 "and LANGSMITH_RUNS_ENDPOINTS."1070 )1071 1072 resolved_mode = _resolve_tracing_mode(tracing_mode, otel_enabled=otel_enabled)1073 self._tracing_mode: TracingMode = resolved_mode1074 env_api_url = _get_langsmith_env_var_uncached("ENDPOINT")1075 env_api_key = _get_langsmith_env_var_uncached("API_KEY")1076 env_workspace_id = _get_langsmith_env_var_uncached("WORKSPACE_ID")1077 profile_config = _profiles.load_profile_client_config()1078 api_url_ = (1079 api_url if api_url is not None else env_api_url or profile_config.api_url1080 )1081 explicit_or_env_api_key = api_key if api_key is not None else env_api_key1082 profile_auth_enabled = api_key is None and env_api_key is None1083 use_profile_oauth = profile_auth_enabled and profile_config.has_oauth1084 api_key_ = (1085 explicit_or_env_api_key1086 if explicit_or_env_api_key is not None1087 else None1088 if use_profile_oauth1089 else profile_config.api_key1090 )1091 workspace_id_ = (1092 workspace_id1093 if workspace_id is not None1094 else env_workspace_id or profile_config.workspace_id1095 )1096 self._oauth_access_token = (1097 profile_config.oauth_access_token if use_profile_oauth else None1098 )1099 self._profile_auth: Optional[_profiles.ProfileAuth] = None1100 self._profile_auth_headers: dict[str, str] = {}1101 1102 self.tracing_sample_rate = _get_tracing_sampling_rate(tracing_sampling_rate)1103 self._filtered_post_uuids: set[uuid.UUID] = set()1104 self._write_api_urls: Mapping[str, Optional[str]] = _get_write_api_urls(1105 api_urls1106 )1107 # Initialize workspace attribute first1108 self._workspace_id = ls_utils.get_workspace_id(workspace_id_)1109 # Store custom headers1110 self._custom_headers = headers or {}1111 1112 if self._write_api_urls:1113 self.api_url = next(iter(self._write_api_urls))1114 self._oauth_access_token = None1115 self._profile_auth = None1116 self._profile_auth_headers = {}1117 self.api_key = self._write_api_urls[self.api_url]1118 else:1119 self.api_url = ls_utils.get_api_url(api_url_)1120 if use_profile_oauth:1121 self._profile_auth = _profiles.ProfileAuth(1122 profile_config,1123 api_key_header=X_API_KEY,1124 )1125 self._profile_auth_headers = self._profile_auth.current_auth_headers()1126 self._oauth_access_token = self._profile_auth.oauth_access_token1127 self.api_key = ls_utils.get_api_key(api_key_)1128 _validate_api_key_if_hosted(1129 self.api_url,1130 self.api_key1131 or self._oauth_access_token1132 or (1133 "profile-auth"1134 if self._profile_auth is not None and self._profile_auth.has_auth1135 else None1136 ),1137 tracing_mode=resolved_mode,1138 )1139 self._write_api_urls = {self.api_url: self.api_key}1140 self.retry_config = retry_config or _default_retry_config()1141 self.timeout_ms = (1142 (timeout_ms, timeout_ms)1143 if isinstance(timeout_ms, int)1144 else (timeout_ms or (10_000, 60_000))1145 )1146 self._timeout = (self.timeout_ms[0] / 1000, self.timeout_ms[1] / 1000)1147 self._web_url = web_url1148 self._tenant_id: Optional[uuid.UUID] = None1149 # Create a session and register a finalizer to close it1150 session_ = session if session else requests.Session()1151 self.session = session_1152 self._info = (1153 info1154 if info is None or isinstance(info, ls_schemas.LangSmithInfo)1155 else ls_schemas.LangSmithInfo(**info)1156 )1157 weakref.finalize(self, close_session, self.session)1158 atexit.register(close_session, session_)1159 self.compressed_traces: Optional[CompressedTraces] = None1160 self._data_available_event: Optional[threading.Event] = None1161 self._futures: Optional[weakref.WeakSet[cf.Future]] = None1162 self._run_ops_buffer: list[tuple[str, dict, dict[str, Optional[str]]]] = []1163 self._run_ops_buffer_lock = threading.Lock()1164 self.otel_exporter: Optional[OTELExporter] = None1165 self._max_batch_size_bytes = max_batch_size_bytes1166 self._multipart_disabled: bool = False1167 self._use_daemon_threads = ls_utils.get_env_var("USE_DAEMON") == "true"1168 1169 if resolved_mode in ("otel", "hybrid"):1170 try:1171 (1172 otel_trace,1173 set_span_in_context,1174 get_otlp_tracer_provider,1175 OTELExporter,1176 ) = _import_otel()1177 1178 existing_provider = otel_trace.get_tracer_provider()1179 tracer = existing_provider.get_tracer(__name__)1180 if otel_tracer_provider is None:1181 if not (1182 isinstance(existing_provider, otel_trace.ProxyTracerProvider)1183 and hasattr(tracer, "_tracer")1184 and isinstance(1185 cast(1186 otel_trace.ProxyTracer, # type: ignore[attr-defined, name-defined]1187 tracer,1188 )._tracer,1189 otel_trace.NoOpTracer,1190 )1191 ):1192 otel_tracer_provider = cast(TracerProvider, existing_provider)1193 else:1194 otel_tracer_provider = get_otlp_tracer_provider()1195 otel_trace.set_tracer_provider(otel_tracer_provider)1196 1197 self.otel_exporter = OTELExporter(tracer_provider=otel_tracer_provider)1198 self._otel_trace = otel_trace1199 self._set_span_in_context = set_span_in_context1200 