Team Ai
Datasetpublic

codekingpro/portable-devtools

sourceHugging Faceupdated 5mo agoView on Hugging Face
1likes15kdownloads
utils.py945 linesDownload Raw Back to langsmith
1"""Generic utility functions."""2 3from __future__ import annotations4 5import contextlib6import contextvars7import copy8import enum9import functools10import logging11import os12import pathlib13import socket14import subprocess15import sys16import threading17import traceback18from collections.abc import Generator, Iterable, Iterator, Mapping, Sequence19from concurrent.futures import Future, ThreadPoolExecutor20from typing import (21    Any,22    Callable,23    Literal,24    Optional,25    TypeVar,26    Union,27    cast,28    overload,29)30from urllib import parse as urllib_parse31 32import httpx33import requests34from typing_extensions import ParamSpec35from urllib3.util import Retry  # type: ignore[import-untyped]36 37from langsmith import schemas as ls_schemas38 39_LOGGER = logging.getLogger(__name__)40 41 42class LangSmithError(Exception):43    """An error occurred while communicating with the LangSmith API."""44 45 46class LangSmithAPIError(LangSmithError):47    """Internal server error while communicating with LangSmith."""48 49 50class LangSmithRequestTimeout(LangSmithError):51    """Client took too long to send request body."""52 53 54class LangSmithUserError(LangSmithError):55    """User error caused an exception when communicating with LangSmith."""56 57 58class LangSmithRateLimitError(LangSmithError):59    """You have exceeded the rate limit for the LangSmith API."""60 61 62class LangSmithAuthError(LangSmithError):63    """Couldn't authenticate with the LangSmith API."""64 65 66class LangSmithNotFoundError(LangSmithError):67    """Couldn't find the requested resource."""68 69 70class LangSmithConflictError(LangSmithError):71    """The resource already exists."""72 73 74class LangSmithConnectionError(LangSmithError):75    """Couldn't connect to the LangSmith API."""76 77 78class LangSmithExceptionGroup(LangSmithError):79    """Port of ExceptionGroup for Py < 3.11."""80 81    def __init__(82        self, *args: Any, exceptions: Sequence[Exception], **kwargs: Any83    ) -> None:84        """Initialize."""85        super().__init__(*args, **kwargs)86        self.exceptions = exceptions87 88 89def get_invalid_prompt_identifier_msg(identifier: str) -> str:90    """Get the error message for an invalid prompt identifier.91 92    Used consistently across the codebase when parsing prompt identifiers fails.93 94    Args:95        identifier: The invalid identifier that was provided.96 97    Returns:98        A formatted error message explaining the valid formats.99    """100    return (101        f'Invalid prompt identifier format: "{identifier}". '102        f"Expected one of:\n"103        f'  - "prompt-name" (for private prompts)\n'104        f'  - "owner/prompt-name" (for prompts with explicit owner)\n'105        f'  - "prompt-name:commit-hash" (with commit reference)\n'106        f'  - "owner/prompt-name:commit-hash" (with owner and commit)'107    )108 109 110## Warning classes111 112 113class LangSmithWarning(UserWarning):114    """Base class for warnings."""115 116 117class LangSmithMissingAPIKeyWarning(LangSmithWarning):118    """Warning for missing API key."""119 120 121def tracing_is_enabled(ctx: Optional[dict] = None) -> Union[bool, Literal["local"]]:122    """Return True if tracing is enabled."""123    # Access global fallbacks via context module to avoid stale references.124    import langsmith._internal._context as _context125    from langsmith.run_helpers import get_current_run_tree, get_tracing_context126 127    tc = ctx or get_tracing_context()128    # You can manually override the environment using context vars.129    # Check that first.130    # Doing this before checking the run tree lets us131    # disable a branch within a trace.132    if tc["enabled"] is not None:133        return tc["enabled"]134    # Next check if we're mid-trace135    if get_current_run_tree():136        return True137    # If a global fallback was configured, use it next.138    if _context._GLOBAL_TRACING_ENABLED is not None:139        return _context._GLOBAL_TRACING_ENABLED140    # Finally, check the global environment141    var_result = get_env_var("TRACING_V2", default=get_env_var("TRACING", default=""))142    return var_result == "true"143 144 145def test_tracking_is_disabled() -> bool:146    """Return True if testing is enabled."""147    return get_env_var("TEST_TRACKING", default="") == "false"148 149 150def xor_args(*arg_groups: tuple[str, ...]) -> Callable:151    """Validate specified keyword args are mutually exclusive."""152 153    def decorator(func: Callable) -> Callable:154        @functools.wraps(func)155        def wrapper(*args: Any, **kwargs: Any) -> Any:156            """Validate exactly one arg in each group is not None."""157            counts = [158                sum(1 for arg in arg_group if kwargs.get(arg) is not None)159                for arg_group in arg_groups160            ]161            invalid_groups = [i for i, count in enumerate(counts) if count != 1]162            if invalid_groups:163                invalid_group_names = [", ".join(arg_groups[i]) for i in invalid_groups]164                raise ValueError(165                    "Exactly one argument in each of the following"166                    " groups must be defined:"167                    f" {', '.join(invalid_group_names)}"168                )169            return func(*args, **kwargs)170 171        return wrapper172 173    return decorator174 175 176def raise_for_status_with_text(177    response: Union[requests.Response, httpx.Response],178) -> None:179    """Raise an error with the response text."""180    try:181        response.raise_for_status()182    except requests.HTTPError as e:183        raise requests.HTTPError(str(e), response.text) from e  # type: ignore[call-arg]184    except httpx.HTTPStatusError as e:185        raise httpx.HTTPStatusError(186            f"{str(e)}: {response.text}",187            request=response.request,  # type: ignore[arg-type]188            response=response,  # type: ignore[arg-type]189        ) from e190 191 192def get_enum_value(enu: Union[enum.Enum, str]) -> str:193    """Get the value of a string enum."""194    if isinstance(enu, enum.Enum):195        return enu.value196    return enu197 198 199@functools.lru_cache(maxsize=1)200def log_once(level: int, message: str) -> None:201    """Log a message at the specified level, but only once."""202    _LOGGER.log(level, message)203 204 205def _get_message_type(message: Mapping[str, Any]) -> str:206    if not message:207        raise ValueError("Message is empty.")208    if "lc" in message:209        if "id" not in message:210            raise ValueError(211                f"Unexpected format for serialized message: {message}"212                " Message does not have an id."213            )214        return message["id"][-1].replace("Message", "").lower()215    else:216        if "type" not in message:217            raise ValueError(218                f"Unexpected format for stored message: {message}"219                " Message does not have a type."220            )221        return message["type"]222 223 224def _get_message_fields(message: Mapping[str, Any]) -> Mapping[str, Any]:225    if not message:226        raise ValueError("Message is empty.")227    if "lc" in message:228        if "kwargs" not in message:229            raise ValueError(230                f"Unexpected format for serialized message: {message}"231                " Message does not have kwargs."232            )233        return message["kwargs"]234    else:235        if "data" not in message:236            raise ValueError(237                f"Unexpected format for stored message: {message}"238                " Message does not have data."239            )240        return message["data"]241 242 243def _convert_message(message: Mapping[str, Any]) -> dict[str, Any]:244    """Extract message from a message object."""245    message_type = _get_message_type(message)246    message_data = _get_message_fields(message)247    return {"type": message_type, "data": message_data}248 249 250def get_messages_from_inputs(inputs: Mapping[str, Any]) -> list[dict[str, Any]]:251    """Extract messages from the given inputs dictionary.252 253    Args:254        inputs: The inputs dictionary.255 256    Returns:257        A list of dictionaries representing the extracted messages.258 259    Raises:260        ValueError: If no message(s) are found in the inputs dictionary.261    """262    if "messages" in inputs:263        return [_convert_message(message) for message in inputs["messages"]]264    if "message" in inputs:265        return [_convert_message(inputs["message"])]266    raise ValueError(f"Could not find message(s) in run with inputs {inputs}.")267 268 269def get_message_generation_from_outputs(outputs: Mapping[str, Any]) -> dict[str, Any]:270    """Retrieve the message generation from the given outputs.271 272    Args:273        outputs: The outputs dictionary.274 275    Returns:276        The message generation.277 278    Raises:279        ValueError: If no generations are found or if multiple generations are present.280    """281    if "generations" not in outputs:282        raise ValueError(f"No generations found in in run with output: {outputs}.")283    generations = outputs["generations"]284    if len(generations) != 1:285        raise ValueError(286            "Chat examples expect exactly one generation."287            f" Found {len(generations)} generations: {generations}."288        )289    first_generation = generations[0]290    if "message" not in first_generation:291        raise ValueError(292            f"Unexpected format for generation: {first_generation}."293            " Generation does not have a message."294        )295    return _convert_message(first_generation["message"])296 297 298def get_prompt_from_inputs(inputs: Mapping[str, Any]) -> str:299    """Retrieve the prompt from the given inputs.300 301    Args:302        inputs: The inputs dictionary.303 304    Returns:305        str: The prompt.306 307    Raises:308        ValueError: If the prompt is not found or if multiple prompts are present.309    """310    if "prompt" in inputs:311        return inputs["prompt"]312    if "prompts" in inputs:313        prompts = inputs["prompts"]314        if len(prompts) == 1:315            return prompts[0]316        raise ValueError(317            f"Multiple prompts in run with inputs {inputs}."318            " Please create example manually."319        )320    raise ValueError(f"Could not find prompt in run with inputs {inputs}.")321 322 323def get_llm_generation_from_outputs(outputs: Mapping[str, Any]) -> str:324    """Get the LLM generation from the outputs."""325    if "generations" not in outputs:326        raise ValueError(f"No generations found in in run with output: {outputs}.")327    generations = outputs["generations"]328    if len(generations) != 1:329        raise ValueError(f"Multiple generations in run: {generations}")330    first_generation = generations[0]331    if "text" not in first_generation:332        raise ValueError(f"No text in generation: {first_generation}")333    return first_generation["text"]334 335 336@functools.lru_cache(maxsize=1)337def get_docker_compose_command() -> list[str]:338    """Get the correct docker compose command for this system."""339    try:340        subprocess.check_call(341            ["docker", "compose", "--version"],342            stdout=subprocess.DEVNULL,343            stderr=subprocess.DEVNULL,344        )345        return ["docker", "compose"]346    except (subprocess.CalledProcessError, FileNotFoundError):347        try:348            subprocess.check_call(349                ["docker-compose", "--version"],350                stdout=subprocess.DEVNULL,351                stderr=subprocess.DEVNULL,352            )353            return ["docker-compose"]354        except (subprocess.CalledProcessError, FileNotFoundError):355            raise ValueError(356                "Neither 'docker compose' nor 'docker-compose'"357                " commands are available. Please install the Docker"358                " server following the instructions for your operating"359                " system at https://docs.docker.com/engine/install/"360            )361 362 363def convert_langchain_message(message: ls_schemas.BaseMessageLike) -> dict:364    """Convert a LangChain message to an example."""365    converted: dict[str, Any] = {366        "type": message.type,367        "data": {"content": message.content},368    }369    # Check for presence of keys in additional_kwargs370    if message.additional_kwargs and len(message.additional_kwargs) > 0:371        converted["data"]["additional_kwargs"] = {**message.additional_kwargs}372    return converted373 374 375def is_base_message_like(obj: object) -> bool:376    """Check if the given object is similar to `BaseMessage`.377 378    Args:379        obj: The object to check.380 381    Returns:382        bool: True if the object is similar to `BaseMessage`, `False` otherwise.383    """384    return all(385        [386            isinstance(getattr(obj, "content", None), str),387            isinstance(getattr(obj, "additional_kwargs", None), dict),388            hasattr(obj, "type") and isinstance(getattr(obj, "type"), str),389        ]390    )391 392 393def is_env_var_truish(value: Optional[str]) -> bool:394    """Check if the given environment variable is truish."""395    if value is None:396        return False397    return is_truish(get_env_var(value))398 399 400@overload401def get_env_var(402    name: str,403    default: str,404    *,405    namespaces: tuple = ("LANGSMITH", "LANGCHAIN"),406) -> str: ...407 408 409@overload410def get_env_var(411    name: str,412    default: None = None,413    *,414    namespaces: tuple = ("LANGSMITH", "LANGCHAIN"),415) -> Optional[str]: ...416 417 418@functools.lru_cache(maxsize=100)419def get_env_var(420    name: str,421    default: Optional[str] = None,422    *,423    namespaces: tuple = ("LANGSMITH", "LANGCHAIN"),424) -> Optional[str]:425    """Retrieve an environment variable from a list of namespaces.426 427    Args:428        name: The name of the environment variable.429        default: The default value to return if the environment variable is not found.430        namespaces: A tuple of namespaces to search for the environment variable.431 432            Defaults to `('LANGSMITH', 'LANGCHAINs')`.433 434    Returns:435        The value of the environment variable if found, otherwise the default value.436    """437    names = [f"{namespace}_{name}" for namespace in namespaces]438    for name in names:439        value = os.environ.get(name)440        if value is not None and value.strip() != "":441            return value442    return default443 444 445@functools.lru_cache(maxsize=1)446def get_tracer_project(return_default_value=True) -> Optional[str]:447    """Get the project name for a LangSmith tracer."""448    return os.environ.get(449        # Hosted LangServe projects get precedence over all other defaults.450        # This is to make sure that we always use the associated project451        # for a hosted langserve deployment even if the customer sets some452        # other project name in their environment.453        "HOSTED_LANGSERVE_PROJECT_NAME",454        get_env_var(455            "PROJECT",456            # This is the legacy name for a LANGCHAIN_PROJECT, so it457            # has lower precedence than LANGCHAIN_PROJECT458            default=get_env_var(459                "SESSION", default="default" if return_default_value else None460            ),461        ),462    )463 464 465class FilterPoolFullWarning(logging.Filter):466    """Filter `urllib3` warnings logged when the connection pool isn't reused."""467 468    def __init__(self, name: str = "", host: str = "") -> None:469        """Initialize the `FilterPoolFullWarning` filter.470 471        Args:472            name: The name of the filter. Defaults to `""`.473            host: The host to filter. Defaults to `""`.474        """475        super().__init__(name)476        self._host = host477 478    def filter(self, record) -> bool:479        """urllib3.connectionpool:Connection pool is full, discarding connection: ..."""480        msg = record.getMessage()481        if "Connection pool is full, discarding connection" not in msg:482            return True483        return self._host not in msg484 485 486class FilterLangSmithRetry(logging.Filter):487    """Filter for retries from this lib."""488 489    def filter(self, record) -> bool:490        """Filter retries from this library."""491        # We re-raise/log manually.492        msg = record.getMessage()493        return "LangSmithRetry" not in msg494 495 496class LangSmithRetry(Retry):497    """Wrapper to filter logs with this name."""498 499 500_FILTER_LOCK = threading.RLock()501 502 503@contextlib.contextmanager504def filter_logs(505    logger: logging.Logger, filters: Sequence[logging.Filter]506) -> Generator[None, None, None]:507    """Temporarily adds specified filters to a logger.508 509    Parameters:510    - logger: The logger to which the filters will be added.511    - filters: A sequence of `logging.Filter` objects to be temporarily added512        to the logger.513    """514    with _FILTER_LOCK:515        for filter in filters:516            logger.addFilter(filter)517    # Not actually perfectly thread-safe, but it's only log filters518    try:519        yield520    finally:521        with _FILTER_LOCK:522            for filter in filters:523                try:524                    logger.removeFilter(filter)525                except BaseException:526                    _LOGGER.warning("Failed to remove filter")527 528 529def get_cache_dir(cache: Optional[str]) -> Optional[str]:530    """Get the testing cache directory.531 532    Args:533        cache: The cache path.534 535    Returns:536        The cache path if provided, otherwise the value from the `LANGSMITH_TEST_CACHE`537        environment variable.538    """539    if cache is not None:540        return cache541    return get_env_var("TEST_CACHE", default=None)542 543 544def filter_request_headers(545    request: Any,546    *,547    ignore_hosts: Optional[Sequence[str]] = None,548    allow_hosts: Optional[Sequence[str]] = None,549) -> Any:550    """Filter request headers based on `ignore_hosts` and `allow_hosts`."""551    # Legacy behavior552    if ignore_hosts and any(request.url.startswith(host) for host in ignore_hosts):553        return None554 555    if allow_hosts:556        try:557            parsed_url = urllib_parse.urlparse(request.url)558        except Exception:559            # If URL parsing fails, don't cache to be safe560            return None561        request_host = parsed_url.hostname or ""562        # Check if request matches any allowed host563        host_matches = any(564            # Handle both full URLs (https://api.openai.com)565            # and hostnames (api.openai.com)566            (567                request.url.startswith(host)568                if host.startswith(("http://", "https://"))569                else request_host == host or request_host.endswith(f".{host}")570            )571            for host in allow_hosts572        )573        if not host_matches:574            return None575 576    request.headers = {}577    return request578 579 580@contextlib.contextmanager581def with_cache(582    path: Union[str, pathlib.Path],583    ignore_hosts: Optional[Sequence[str]] = None,584    allow_hosts: Optional[Sequence[str]] = None,585) -> Generator[None, None, None]:586    """Use a cache for requests."""587    try:588        import vcr  # type: ignore[import-untyped]589    except ImportError:590        raise ImportError(591            "vcrpy is required to use caching. Install with:"592            'pip install -U "langsmith[vcr]"'593        )594    # Fix concurrency issue in vcrpy's patching595    from langsmith._internal import _patch as patch_urllib3596 597    patch_urllib3.patch_urllib3()598 599    cache_dir, cache_file = os.path.split(path)600 601    ls_vcr = vcr.VCR(602        serializer=(603            "yaml"604            if cache_file.endswith(".yaml") or cache_file.endswith(".yml")605            else "json"606        ),607        cassette_library_dir=cache_dir,608        # Replay previous requests, record new ones609        # TODO: Support other modes610        record_mode="new_episodes",611        match_on=["uri", "method", "path", "body"],612        filter_headers=["authorization", "Set-Cookie"],613        before_record_request=lambda request: filter_request_headers(614            request, ignore_hosts=ignore_hosts, allow_hosts=allow_hosts615        ),616    )617    with ls_vcr.use_cassette(cache_file):618        yield619 620 621@contextlib.contextmanager622def with_optional_cache(623    path: Optional[Union[str, pathlib.Path]],624    ignore_hosts: Optional[Sequence[str]] = None,625    allow_hosts: Optional[Sequence[str]] = None,626) -> Generator[None, None, None]:627    """Use a cache for requests."""628    if path is not None:629        with with_cache(path, ignore_hosts, allow_hosts):630            yield631    else:632        yield633 634 635def _format_exc() -> str:636    # Used internally to format exceptions without cluttering the traceback637    tb_lines = traceback.format_exception(*sys.exc_info())638    filtered_lines = [line for line in tb_lines if "langsmith/" not in line]639    return "".join(filtered_lines)640 641 642T = TypeVar("T")643 644 645def _middle_copy(646    val: T, memo: dict[int, Any], max_depth: int = 4, _depth: int = 0647) -> T:648    cls = type(val)649 650    copier = getattr(cls, "__deepcopy__", None)651    if copier is not None:652        try:653            return copier(memo)654        except BaseException:655            pass656    if _depth >= max_depth:657        return val658    if isinstance(val, dict):659        return {  # type: ignore[return-value]660            _middle_copy(k, memo, max_depth, _depth + 1): _middle_copy(661                v, memo, max_depth, _depth + 1662            )663            for k, v in val.items()664        }665    if isinstance(val, list):666        return [_middle_copy(item, memo, max_depth, _depth + 1) for item in val]  # type: ignore[return-value]667    if isinstance(val, tuple):668        return tuple(_middle_copy(item, memo, max_depth, _depth + 1) for item in val)  # type: ignore[return-value]669    if isinstance(val, set):670        return {_middle_copy(item, memo, max_depth, _depth + 1) for item in val}  # type: ignore[return-value]671 672    return val673 674 675def deepish_copy(val: T) -> T:676    """Deep copy a value with a compromise for uncopyable objects.677 678    Args:679        val: The value to be deep copied.680 681    Returns:682        The deep copied value.683    """684    memo: dict[int, Any] = {}685    try:686        return copy.deepcopy(val, memo)687    except BaseException as e:688        # Generators, locks, etc. cannot be copied689        # and raise a TypeError (mentioning pickling, since the dunder methods)690        # are re-used for copying. We'll try to do a compromise and copy691        # what we can692        _LOGGER.debug("Failed to deepcopy input: %s", repr(e))693        return _middle_copy(val, memo)694 695 696def is_version_greater_or_equal(current_version: str, target_version: str) -> bool:697    """Check if the current version is greater or equal to the target version."""698    from packaging import version699 700    current = version.parse(current_version)701    target = version.parse(target_version)702    return current >= target703 704 705def parse_prompt_identifier(identifier: str) -> tuple[str, str, str]:706    """Parse a string in the format of `owner/name:hash`, `name:hash`, `owner/name`, or `name`.707 708    Args:709        identifier: The prompt identifier to parse.710 711    Returns:712        A tuple containing `(owner, name, hash)`.713 714    Raises:715        ValueError: If the identifier doesn't match the expected formats.716    """  # noqa: E501717    if (718        not identifier719        or identifier.count("/") > 1720        or identifier.startswith("/")721        or identifier.endswith("/")722    ):723        raise ValueError(get_invalid_prompt_identifier_msg(identifier))724 725    parts = identifier.split(":", 1)726    owner_name = parts[0]727    commit = parts[1] if len(parts) > 1 else "latest"728 729    if "/" in owner_name:730        owner, name = owner_name.split("/", 1)731        if not owner or not name:732            raise ValueError(get_invalid_prompt_identifier_msg(identifier))733        return owner, name, commit734    else:735        if not owner_name:736            raise ValueError(get_invalid_prompt_identifier_msg(identifier))737        return "-", owner_name, commit738 739 740def parse_hub_identifier(identifier: str) -> tuple[str, str, str]:741    """Parse a hub repo identifier (``owner/name:hash``, ``name``, etc.).742 743    Agents, skills, and prompts share the same identifier grammar on Hub.744 745    Args:746        identifier: The hub identifier to parse.747 748    Returns:749        A tuple containing ``(owner, name, hash)``.750 751    Raises:752        ValueError: If the identifier doesn't match the expected formats.753    """754    return parse_prompt_identifier(identifier)755 756 757P = ParamSpec("P")758 759 760class ContextThreadPoolExecutor(ThreadPoolExecutor):761    """ThreadPoolExecutor that copies the context to the child thread."""762 763    def submit(  # type: ignore[override]764        self,765        func: Callable[P, T],766        *args: P.args,767        **kwargs: P.kwargs,768    ) -> Future[T]:769        """Submit a function to the executor.770 771        Args:772            func (Callable[..., T]): The function to submit.773            *args (Any): The positional arguments to the function.774            **kwargs (Any): The keyword arguments to the function.775 776        Returns:777            Future[T]: The future for the function.778        """779        return super().submit(780            cast(781                Callable[..., T],782                functools.partial(783                    contextvars.copy_context().run, func, *args, **kwargs784                ),785            )786        )787 788    def map(789        self,790        fn: Callable[..., T],791        *iterables: Iterable[Any],792        timeout: Optional[float] = None,793        chunksize: int = 1,794    ) -> Iterator[T]:795        """Return an iterator equivalent to stdlib map.796 797        Each function will receive its own copy of the context from the parent thread.798 799        Args:800            fn: A callable that will take as many arguments as there are801                passed iterables.802            timeout: The maximum number of seconds to wait. If None, then there803                is no limit on the wait time.804            chunksize: The size of the chunks the iterable will be broken into805                before being passed to a child process. This argument is only806                used by ProcessPoolExecutor; it is ignored by807                ThreadPoolExecutor.808 809        Returns:810            An iterator equivalent to: map(func, *iterables) but the calls may811            be evaluated out-of-order.812 813        Raises:814            TimeoutError: If the entire result iterator could not be generated815                before the given timeout.816            Exception: If fn(*args) raises for any values.817        """818        contexts = [contextvars.copy_context() for _ in range(len(iterables[0]))]  # type: ignore[arg-type]819 820        def _wrapped_fn(*args: Any) -> T:821            return contexts.pop().run(fn, *args)822 823        return super().map(824            _wrapped_fn,825            *iterables,826            timeout=timeout,827            chunksize=chunksize,828        )829 830 831def get_api_url(api_url: Optional[str]) -> str:832    """Get the LangSmith API URL from the environment or the given value."""833    _api_url = api_url or cast(834        str,835        get_env_var(836            "ENDPOINT",837            default="https://api.smith.langchain.com",838        ),839    )840    if not _api_url.strip():841        raise LangSmithUserError("LangSmith API URL cannot be empty")842    return _api_url.strip().strip('"').strip("'").rstrip("/")843 844 845def get_api_key(api_key: Optional[str]) -> Optional[str]:846    """Get the API key from the environment or the given value."""847    api_key_ = api_key if api_key is not None else get_env_var("API_KEY", default=None)848    if api_key_ is None or not api_key_.strip():849        return None850    return api_key_.strip().strip('"').strip("'")851 852 853def get_workspace_id(workspace_id: Optional[str]) -> Optional[str]:854    """Get workspace ID."""855    workspace_id_ = (856        workspace_id857        if workspace_id is not None858        else get_env_var("WORKSPACE_ID", default=None)859    )860    if workspace_id_ is None or not workspace_id_.strip():861        return None862    return workspace_id_.strip().strip('"').strip("'")863 864 865def _is_localhost(url: str) -> bool:866    """Check if the URL is localhost.867 868    Parameters869    ----------870    url : str871        The URL to check.872 873    Returns:874    -------875    bool876        True if the URL is localhost, False otherwise.877    """878    try:879        netloc = urllib_parse.urlsplit(url).netloc.split(":")[0]880        ip = socket.gethostbyname(netloc)881        return ip == "127.0.0.1" or ip.startswith("0.0.0.0") or ip.startswith("::")882    except socket.gaierror:883        return False884 885 886@functools.lru_cache(maxsize=2)887def get_host_url(web_url: Optional[str], api_url: str):888    """Get the host URL based on the web URL or API URL."""889    if web_url:890        return web_url891    parsed_url = urllib_parse.urlparse(api_url)892    if _is_localhost(api_url):893        link = "http://localhost"894    elif str(parsed_url.path).endswith("/api"):895        new_path = str(parsed_url.path).rsplit("/api", 1)[0]896        link = urllib_parse.urlunparse(parsed_url._replace(path=new_path))897    elif str(parsed_url.path).endswith("/api/v1"):898        new_path = str(parsed_url.path).rsplit("/api/v1", 1)[0]899        link = urllib_parse.urlunparse(parsed_url._replace(path=new_path))900    elif str(parsed_url.netloc).startswith("eu."):901        link = "https://eu.smith.langchain.com"902    elif str(parsed_url.netloc).startswith("aws."):903        link = "https://aws.smith.langchain.com"904    elif str(parsed_url.netloc).startswith("apac."):905        link = "https://apac.smith.langchain.com"906    elif str(parsed_url.netloc).startswith("dev."):907        link = "https://dev.smith.langchain.com"908    elif str(parsed_url.netloc).startswith("beta."):909        link = "https://beta.smith.langchain.com"910    else:911        link = "https://smith.langchain.com"912    return link913 914 915def _get_function_name(fn: Callable, depth: int = 0) -> str:916    if depth > 2 or not callable(fn):917        return str(fn)918 919    if hasattr(fn, "__name__"):920        return fn.__name__921 922    if isinstance(fn, functools.partial):923        return _get_function_name(fn.func, depth + 1)924 925    if hasattr(fn, "__call__"):926        if hasattr(fn, "__class__") and hasattr(fn.__class__, "__name__"):927            return fn.__class__.__name__928        return _get_function_name(fn.__call__, depth + 1)929 930    return str(fn)931 932 933def is_truish(val: Any) -> bool:934    """Check if the value is truish.935 936    Args:937        val (Any): The value to check.938 939    Returns:940        bool: True if the value is truish, False otherwise.941    """942    if isinstance(val, str):943        return val.lower() == "true" or val == "1"944    return bool(val)945 
codekingpro/portable-devtools · Team Ai