Files
2024-03-03 15:43:22 -08:00

968 lines
33 KiB
Python

"""Decorator for creating a run tree from functions."""
from __future__ import annotations
import contextlib
import contextvars
import datetime
import functools
import inspect
import logging
import traceback
import uuid
import warnings
from typing import (
TYPE_CHECKING,
Any,
AsyncGenerator,
Awaitable,
Callable,
Dict,
Generator,
Generic,
List,
Mapping,
Optional,
Protocol,
TypedDict,
TypeVar,
Union,
cast,
overload,
runtime_checkable,
)
from langsmith import client as ls_client
from langsmith import run_trees, utils
if TYPE_CHECKING:
from langchain.schema.runnable import Runnable
logger = logging.getLogger(__name__)
_PARENT_RUN_TREE = contextvars.ContextVar[Optional[run_trees.RunTree]](
"_PARENT_RUN_TREE", default=None
)
_PROJECT_NAME = contextvars.ContextVar[Optional[str]]("_PROJECT_NAME", default=None)
_TAGS = contextvars.ContextVar[Optional[List[str]]]("_TAGS", default=None)
_METADATA = contextvars.ContextVar[Optional[Dict[str, Any]]]("_METADATA", default=None)
def get_current_run_tree() -> Optional[run_trees.RunTree]:
"""Get the current run tree context."""
return _PARENT_RUN_TREE.get()
get_run_tree_context = get_current_run_tree
def _is_traceable_function(func: Callable) -> bool:
return getattr(func, "__langsmith_traceable__", False)
def is_traceable_function(func: Callable) -> bool:
"""Check if a function is @traceable decorated."""
return (
_is_traceable_function(func)
or (isinstance(func, functools.partial) and _is_traceable_function(func.func))
or (hasattr(func, "__call__") and _is_traceable_function(func.__call__))
)
def is_async(func: Callable) -> bool:
"""Inspect function or wrapped function to see if it is async."""
return inspect.iscoroutinefunction(func) or (
hasattr(func, "__wrapped__") and inspect.iscoroutinefunction(func.__wrapped__)
)
def _get_inputs(
signature: inspect.Signature, *args: Any, **kwargs: Any
) -> Dict[str, Any]:
"""Return a dictionary of inputs from the function signature."""
bound = signature.bind_partial(*args, **kwargs)
bound.apply_defaults()
arguments = dict(bound.arguments)
arguments.pop("self", None)
arguments.pop("cls", None)
for param_name, param in signature.parameters.items():
if param.kind == inspect.Parameter.VAR_KEYWORD:
# Update with the **kwargs, and remove the original entry
# This is to help flatten out keyword arguments
if param_name in arguments:
arguments.update(arguments[param_name])
arguments.pop(param_name)
return arguments
class LangSmithExtra(TypedDict, total=False):
"""Any additional info to be injected into the run dynamically."""
reference_example_id: Optional[ls_client.ID_TYPE]
run_extra: Optional[Dict]
run_tree: Optional[run_trees.RunTree]
project_name: Optional[str]
metadata: Optional[Dict[str, Any]]
tags: Optional[List[str]]
run_id: Optional[ls_client.ID_TYPE]
client: Optional[ls_client.Client]
class _TraceableContainer(TypedDict, total=False):
"""Typed response when initializing a run a traceable."""
new_run: Optional[run_trees.RunTree]
project_name: Optional[str]
outer_project: Optional[str]
outer_metadata: Optional[Dict[str, Any]]
outer_tags: Optional[List[str]]
class _ContainerInput(TypedDict, total=False):
"""Typed response when initializing a run a traceable."""
extra_outer: Optional[Dict]
name: Optional[str]
metadata: Optional[Dict[str, Any]]
tags: Optional[List[str]]
client: Optional[ls_client.Client]
reduce_fn: Optional[Callable]
project_name: Optional[str]
run_type: ls_client.RUN_TYPE_T
process_inputs: Optional[Callable[[dict], dict]]
def _container_end(
container: _TraceableContainer,
outputs: Optional[Any] = None,
error: Optional[str] = None,
):
"""End the run."""
run_tree = container.get("new_run")
if run_tree is None:
# Tracing disabled
return
outputs_ = outputs if isinstance(outputs, dict) else {"output": outputs}
run_tree.end(outputs=outputs_, error=error)
run_tree.patch()
if error:
try:
logger.info(f"See trace: {run_tree.get_url()}")
except Exception:
pass
def _collect_extra(extra_outer: dict, langsmith_extra: LangSmithExtra) -> dict:
run_extra = langsmith_extra.get("run_extra", None)
if run_extra:
extra_inner = {**extra_outer, **run_extra}
else:
extra_inner = extra_outer
return extra_inner
def _setup_run(
func: Callable,
container_input: _ContainerInput,
langsmith_extra: Optional[LangSmithExtra] = None,
args: Any = None,
kwargs: Any = None,
) -> _TraceableContainer:
"""Create a new run or create_child() if run is passed in kwargs."""
extra_outer = container_input.get("extra_outer") or {}
name = container_input.get("name")
metadata = container_input.get("metadata")
tags = container_input.get("tags")
client = container_input.get("client")
run_type = container_input.get("run_type") or "chain"
outer_project = _PROJECT_NAME.get()
langsmith_extra = langsmith_extra or LangSmithExtra()
parent_run_ = langsmith_extra.get("run_tree") or get_run_tree_context()
selected_project = (
_PROJECT_NAME.get() # From parent trace
or langsmith_extra.get("project_name") # at invocation time
or container_input["project_name"] # at decorator time
or utils.get_tracer_project() # default
)
if not parent_run_ and not utils.tracing_is_enabled():
utils.log_once(
logging.DEBUG, "LangSmith tracing is disabled, returning original function."
)
return _TraceableContainer(
new_run=None,
project_name=selected_project,
outer_project=outer_project,
outer_metadata=None,
outer_tags=None,
)
signature = inspect.signature(func)
name_ = name or func.__name__
docstring = func.__doc__
extra_inner = _collect_extra(extra_outer, langsmith_extra)
outer_metadata = _METADATA.get()
metadata_ = {
**(langsmith_extra.get("metadata") or {}),
**(outer_metadata or {}),
}
_METADATA.set(metadata_)
metadata_.update(metadata or {})
metadata_["ls_method"] = "traceable"
extra_inner["metadata"] = metadata_
try:
inputs = _get_inputs(signature, *args, **kwargs)
except TypeError as e:
logger.debug(f"Failed to infer inputs for {name_}: {e}")
inputs = {"args": args, "kwargs": kwargs}
process_inputs = container_input.get("process_inputs")
if process_inputs:
try:
inputs = process_inputs(inputs)
except Exception as e:
logger.error(f"Failed to filter inputs for {name_}: {e}")
outer_tags = _TAGS.get()
tags_ = (langsmith_extra.get("tags") or []) + (outer_tags or [])
_TAGS.set(tags_)
tags_ += tags or []
id_ = langsmith_extra.get("run_id", uuid.uuid4())
client_ = langsmith_extra.get("client", client)
if parent_run_ is not None:
new_run = parent_run_.create_child(
name=name_,
run_type=run_type,
serialized={
"name": name,
"signature": str(signature),
"doc": docstring,
},
inputs=inputs,
tags=tags_,
extra=extra_inner,
run_id=id_,
)
else:
new_run = run_trees.RunTree(
id=id_,
name=name_,
serialized={
"name": name,
"signature": str(signature),
"doc": docstring,
},
inputs=inputs,
run_type=run_type,
reference_example_id=langsmith_extra.get("reference_example_id"),
project_name=selected_project,
extra=extra_inner,
tags=tags_,
client=client_,
)
try:
new_run.post()
except Exception as e:
logger.error(f"Failed to post run {new_run.id}: {e}")
response_container = _TraceableContainer(
new_run=new_run,
project_name=selected_project,
outer_project=outer_project,
outer_metadata=outer_metadata,
outer_tags=outer_tags,
)
_PROJECT_NAME.set(response_container["project_name"])
_PARENT_RUN_TREE.set(response_container["new_run"])
return response_container
R = TypeVar("R", covariant=True)
_VALID_RUN_TYPES = {
"tool",
"chain",
"llm",
"retriever",
"embedding",
"prompt",
"parser",
}
@runtime_checkable
class SupportsLangsmithExtra(Protocol, Generic[R]):
"""Implementations of this Protoc accept an optional langsmith_extra parameter.
Args:
*args: Variable length arguments.
langsmith_extra (Optional[Dict[str, Any]]): Optional dictionary of
additional parameters for Langsmith.
**kwargs: Keyword arguments.
Returns:
R: The return type of the callable.
"""
def __call__(
self,
*args: Any,
langsmith_extra: Optional[Dict[str, Any]] = None,
**kwargs: Any,
) -> R:
"""Call the instance when it is called as a function.
Args:
*args: Variable length argument list.
langsmith_extra: Optional dictionary containing additional
parameters specific to Langsmith.
**kwargs: Arbitrary keyword arguments.
Returns:
R: The return value of the method.
"""
...
@overload
def traceable(
func: Callable[..., R],
) -> Callable[..., R]:
...
@overload
def traceable(
run_type: ls_client.RUN_TYPE_T = "chain",
*,
name: Optional[str] = None,
metadata: Optional[Mapping[str, Any]] = None,
tags: Optional[List[str]] = None,
client: Optional[ls_client.Client] = None,
reduce_fn: Optional[Callable] = None,
project_name: Optional[str] = None,
process_inputs: Optional[Callable[[dict], dict]] = None,
) -> Callable[[Callable[..., R]], SupportsLangsmithExtra[R]]:
...
def traceable(
*args: Any,
**kwargs: Any,
) -> Union[Callable, Callable[[Callable], Callable]]:
"""Trace a function with langsmith.
Args:
run_type: The type of run (span) to create. Examples: llm, chain, tool, prompt,
retriever, etc. Defaults to "chain".
name: The name of the run. Defaults to the function name.
metadata: The metadata to add to the run. Defaults to None.
tags: The tags to add to the run. Defaults to None.
client: The client to use for logging the run to LangSmith. Defaults to
None, which will use the default client.
reduce_fn: A function to reduce the output of the function if the function
returns a generator. Defaults to None, which means the values will be
logged as a list. Note: if the iterator is never exhausted (e.g.
the function returns an infinite generator), this will never be
called, and the run itself will be stuck in a pending state.
project_name: The name of the project to log the run to. Defaults to None,
which will use the default project.
process_inputs: A function to filter the inputs to the run. Defaults to None.
Returns:
Union[Callable, Callable[[Callable], Callable]]: The decorated function.
Note:
- Requires that LANGCHAIN_TRACING_V2 be set to 'true' in the environment.
Examples:
.. code-block:: python
import httpx
import asyncio
from typing import Iterable
from langsmith import traceable, Client
# Basic usage:
@traceable
def my_function(x: float, y: float) -> float:
return x + y
my_function(5, 6)
@traceable
async def my_async_function(query_params: dict) -> dict:
async with httpx.AsyncClient() as http_client:
response = await http_client.get(
"https://api.example.com/data",
params=query_params,
)
return response.json()
asyncio.run(my_async_function({"param": "value"}))
# Streaming data with a generator:
@traceable
def my_generator(n: int) -> Iterable:
for i in range(n):
yield i
for item in my_generator(5):
print(item)
# Async streaming data
@traceable
async def my_async_generator(query_params: dict) -> Iterable:
async with httpx.AsyncClient() as http_client:
response = await http_client.get(
"https://api.example.com/data",
params=query_params,
)
for item in response.json():
yield item
async def async_code():
async for item in my_async_generator({"param": "value"}):
print(item)
asyncio.run(async_code())
# Specifying a run type and name:
@traceable(name="CustomName", run_type="tool")
def another_function(a: float, b: float) -> float:
return a * b
another_function(5, 6)
# Logging with custom metadata and tags:
@traceable(
metadata={"version": "1.0", "author": "John Doe"},
tags=["beta", "test"]
)
def tagged_function(x):
return x**2
tagged_function(5)
# Specifying a custom client and project name:
custom_client = Client(api_key="your_api_key")
@traceable(client=custom_client, project_name="My Special Project")
def project_specific_function(data):
return data
project_specific_function({"data": "to process"})
# Manually passing langsmith_extra:
@traceable
def manual_extra_function(x):
return x**2
manual_extra_function(5, langsmith_extra={"metadata": {"version": "1.0"}})
"""
run_type: ls_client.RUN_TYPE_T = (
args[0]
if args and isinstance(args[0], str)
else (kwargs.pop("run_type", None) or "chain")
)
if run_type not in _VALID_RUN_TYPES:
warnings.warn(
f"Unrecognized run_type: {run_type}. Must be one of: {_VALID_RUN_TYPES}."
f" Did you mean @traceable(name='{run_type}')?"
)
if len(args) > 1:
warnings.warn(
"The `traceable()` decorator only accepts one positional argument, "
"which should be the run_type. All other arguments should be passed "
"as keyword arguments."
)
if "extra" in kwargs:
warnings.warn(
"The `extra` keyword argument is deprecated. Please use `metadata` "
"instead.",
DeprecationWarning,
)
reduce_fn = kwargs.pop("reduce_fn", None)
container_input = _ContainerInput(
# TODO: Deprecate raw extra
extra_outer=kwargs.pop("extra", None),
name=kwargs.pop("name", None),
metadata=kwargs.pop("metadata", None),
tags=kwargs.pop("tags", None),
client=kwargs.pop("client", None),
project_name=kwargs.pop("project_name", None),
run_type=run_type,
process_inputs=kwargs.pop("process_inputs", None),
)
if kwargs:
warnings.warn(
f"The following keyword arguments are not recognized and will be ignored: "
f"{sorted(kwargs.keys())}.",
DeprecationWarning,
)
def decorator(func: Callable):
@functools.wraps(func)
async def async_wrapper(
*args: Any,
langsmith_extra: Optional[LangSmithExtra] = None,
**kwargs: Any,
) -> Any:
"""Async version of wrapper function."""
context_run = get_run_tree_context()
run_container = _setup_run(
func,
container_input=container_input,
langsmith_extra=langsmith_extra,
args=args,
kwargs=kwargs,
)
func_accepts_parent_run = (
inspect.signature(func).parameters.get("run_tree", None) is not None
)
try:
if func_accepts_parent_run:
function_result = await func(
*args, run_tree=run_container["new_run"], **kwargs
)
else:
function_result = await func(*args, **kwargs)
except Exception as e:
stacktrace = traceback.format_exc()
_container_end(run_container, error=stacktrace)
raise e
finally:
_PARENT_RUN_TREE.set(context_run)
_PROJECT_NAME.set(run_container["outer_project"])
_TAGS.set(run_container["outer_tags"])
_METADATA.set(run_container["outer_metadata"])
_container_end(run_container, outputs=function_result)
return function_result
@functools.wraps(func)
async def async_generator_wrapper(
*args: Any, langsmith_extra: Optional[LangSmithExtra] = None, **kwargs: Any
) -> AsyncGenerator:
context_run = get_run_tree_context()
run_container = _setup_run(
func,
container_input=container_input,
langsmith_extra=langsmith_extra,
args=args,
kwargs=kwargs,
)
func_accepts_parent_run = (
inspect.signature(func).parameters.get("run_tree", None) is not None
)
results: List[Any] = []
try:
if func_accepts_parent_run:
async_gen_result = func(
*args, run_tree=run_container["new_run"], **kwargs
)
else:
# TODO: Nesting is ambiguous if a nested traceable function is only
# called mid-generation. Need to explicitly accept run_tree to get
# around this.
async_gen_result = func(*args, **kwargs)
_PARENT_RUN_TREE.set(context_run)
_PROJECT_NAME.set(run_container["outer_project"])
_TAGS.set(run_container["outer_tags"])
_METADATA.set(run_container["outer_metadata"])
# Can't iterate through if it's a coroutine
if inspect.iscoroutine(async_gen_result):
async_gen_result = await async_gen_result
async for item in async_gen_result:
if run_type == "llm":
if run_container["new_run"]:
run_container["new_run"].add_event(
{
"name": "new_token",
"time": datetime.datetime.now(
datetime.timezone.utc
).isoformat(),
"kwargs": {"token": item},
}
)
results.append(item)
yield item
except BaseException as e:
stacktrace = traceback.format_exc()
_container_end(run_container, error=stacktrace)
raise e
finally:
_PARENT_RUN_TREE.set(context_run)
_PROJECT_NAME.set(run_container["outer_project"])
_TAGS.set(run_container["outer_tags"])
_METADATA.set(run_container["outer_metadata"])
if results:
if reduce_fn:
try:
function_result = reduce_fn(results)
except Exception as e:
logger.error(e)
function_result = results
else:
function_result = results
else:
function_result = None
_container_end(run_container, outputs=function_result)
@functools.wraps(func)
def wrapper(
*args: Any,
langsmith_extra: Optional[LangSmithExtra] = None,
**kwargs: Any,
) -> Any:
"""Create a new run or create_child() if run is passed in kwargs."""
context_run = get_run_tree_context()
run_container = _setup_run(
func,
container_input=container_input,
langsmith_extra=langsmith_extra,
args=args,
kwargs=kwargs,
)
func_accepts_parent_run = (
inspect.signature(func).parameters.get("run_tree", None) is not None
)
try:
if func_accepts_parent_run:
function_result = func(
*args, run_tree=run_container["new_run"], **kwargs
)
else:
function_result = func(*args, **kwargs)
except BaseException as e:
stacktrace = traceback.format_exc()
_container_end(run_container, error=stacktrace)
raise e
finally:
_PARENT_RUN_TREE.set(context_run)
_PROJECT_NAME.set(run_container["outer_project"])
_TAGS.set(run_container["outer_tags"])
_METADATA.set(run_container["outer_metadata"])
_container_end(run_container, outputs=function_result)
return function_result
@functools.wraps(func)
def generator_wrapper(
*args: Any, langsmith_extra: Optional[LangSmithExtra] = None, **kwargs: Any
) -> Any:
context_run = get_run_tree_context()
run_container = _setup_run(
func,
container_input=container_input,
langsmith_extra=langsmith_extra,
args=args,
kwargs=kwargs,
)
func_accepts_parent_run = (
inspect.signature(func).parameters.get("run_tree", None) is not None
)
results: List[Any] = []
try:
if func_accepts_parent_run:
generator_result = func(
*args, run_tree=run_container["new_run"], **kwargs
)
else:
# TODO: Nesting is ambiguous if a nested traceable function is only
# called mid-generation. Need to explicitly accept run_tree to get
# around this.
generator_result = func(*args, **kwargs)
for item in generator_result:
if run_type == "llm":
if run_container["new_run"]:
run_container["new_run"].add_event(
{
"name": "new_token",
"time": datetime.datetime.now(
datetime.timezone.utc
).isoformat(),
"kwargs": {"token": item},
}
)
results.append(item)
try:
yield item
except GeneratorExit:
break
except BaseException as e:
stacktrace = traceback.format_exc()
_container_end(run_container, error=stacktrace)
raise e
finally:
_PARENT_RUN_TREE.set(context_run)
_PROJECT_NAME.set(run_container["outer_project"])
_TAGS.set(run_container["outer_tags"])
_METADATA.set(run_container["outer_metadata"])
if results:
if reduce_fn:
try:
function_result = reduce_fn(results)
except Exception as e:
logger.error(e)
function_result = results
else:
function_result = results
else:
function_result = None
_container_end(run_container, outputs=function_result)
if inspect.isasyncgenfunction(func):
selected_wrapper: Callable = async_generator_wrapper
elif is_async(func):
if reduce_fn:
selected_wrapper = async_generator_wrapper
else:
selected_wrapper = async_wrapper
elif reduce_fn or inspect.isgeneratorfunction(func):
selected_wrapper = generator_wrapper
else:
selected_wrapper = wrapper
setattr(selected_wrapper, "__langsmith_traceable__", True)
return selected_wrapper
# If the decorator is called with no arguments, then it's being used as a
# decorator, so we return the decorator function
if len(args) == 1 and callable(args[0]) and not kwargs:
return decorator(args[0])
# Else it's being used as a decorator factory, so we return the decorator
return decorator
@contextlib.contextmanager
def trace(
name: str,
run_type: ls_client.RUN_TYPE_T = "chain",
*,
inputs: Optional[Dict] = None,
extra: Optional[Dict] = None,
project_name: Optional[str] = None,
run_tree: Optional[run_trees.RunTree] = None,
tags: Optional[List[str]] = None,
metadata: Optional[Mapping[str, Any]] = None,
**kwargs: Any,
) -> Generator[run_trees.RunTree, None, None]:
"""Context manager for creating a run tree."""
if kwargs:
# In case someone was passing an executor before.
warnings.warn(
"The `trace` context manager no longer supports the following kwargs: "
f"{sorted(kwargs.keys())}.",
DeprecationWarning,
)
outer_tags = _TAGS.get()
outer_metadata = _METADATA.get()
outer_project = _PROJECT_NAME.get() or utils.get_tracer_project()
parent_run_ = get_run_tree_context() if run_tree is None else run_tree
# Merge and set context variables
tags_ = sorted(set((tags or []) + (outer_tags or [])))
_TAGS.set(tags_)
metadata = {**(metadata or {}), **(outer_metadata or {}), "ls_method": "trace"}
_METADATA.set(metadata)
extra_outer = extra or {}
extra_outer["metadata"] = metadata
project_name_ = project_name or outer_project
if parent_run_ is not None:
new_run = parent_run_.create_child(
name=name,
run_type=run_type,
extra=extra_outer,
inputs=inputs,
tags=tags_,
)
else:
new_run = run_trees.RunTree(
name=name,
run_type=run_type,
extra=extra_outer,
project_name=project_name_,
inputs=inputs or {},
tags=tags_,
)
new_run.post()
_PARENT_RUN_TREE.set(new_run)
_PROJECT_NAME.set(project_name_)
try:
yield new_run
except (Exception, KeyboardInterrupt, BaseException) as e:
tb = traceback.format_exc()
new_run.end(error=tb)
new_run.patch()
raise e
finally:
_PARENT_RUN_TREE.set(parent_run_)
_PROJECT_NAME.set(outer_project)
_TAGS.set(outer_tags)
_METADATA.set(outer_metadata)
if new_run.end_time is None:
# User didn't call end() on the run, so we'll do it for them
new_run.end()
new_run.patch()
def as_runnable(traceable_fn: Callable) -> Runnable:
"""Convert a function wrapped by the LangSmith @traceable decorator to a Runnable.
Args:
traceable_fn (Callable): The function wrapped by the @traceable decorator.
Returns:
Runnable: A Runnable object that maintains a consistent LangSmith
tracing context.
Raises:
ImportError: If langchain module is not installed.
ValueError: If the provided function is not wrapped by the @traceable decorator.
Example:
>>> @traceable
... def my_function(input_data):
... # Function implementation
... pass
...
>>> runnable = as_runnable(my_function)
"""
try:
from langchain.callbacks.manager import (
AsyncCallbackManager,
CallbackManager,
)
from langchain.callbacks.tracers.langchain import LangChainTracer
from langchain.schema.runnable import RunnableConfig, RunnableLambda
from langchain.schema.runnable.utils import Input, Output
except ImportError as e:
raise ImportError(
"as_runnable requires langchain to be installed. "
"You can install it with `pip install langchain`."
) from e
if not is_traceable_function(traceable_fn):
try:
fn_src = inspect.getsource(traceable_fn)
except Exception:
fn_src = "<source unavailable>"
raise ValueError(
f"as_runnable expects a function wrapped by the LangSmith"
f" @traceable decorator. Got {traceable_fn} defined as:\n{fn_src}"
)
class RunnableTraceable(RunnableLambda):
"""Converts a @traceable decorated function to a Runnable.
This helps maintain a consistent LangSmith tracing context.
"""
def __init__(
self,
func: Callable,
afunc: Optional[Callable[..., Awaitable[Output]]] = None,
) -> None:
wrapped: Optional[Callable[[Input], Output]] = None
awrapped = self._wrap_async(afunc)
if is_async(func):
if awrapped is not None:
raise TypeError(
"Func was provided as a coroutine function, but afunc was "
"also provided. If providing both, func should be a regular "
"function to avoid ambiguity."
)
wrapped = cast(Callable[[Input], Output], self._wrap_async(func))
elif is_traceable_function(func):
wrapped = cast(Callable[[Input], Output], self._wrap_sync(func))
if wrapped is None:
raise ValueError(
f"{self.__class__.__name__} expects a function wrapped by"
" the LangSmith"
f" @traceable decorator. Got {func}"
)
super().__init__(
wrapped,
cast(
Optional[Callable[[Input], Awaitable[Output]]],
awrapped,
),
)
@staticmethod
def _configure_run_tree(callback_manager: Any) -> Optional[run_trees.RunTree]:
run_tree: Optional[run_trees.RunTree] = None
if isinstance(callback_manager, (CallbackManager, AsyncCallbackManager)):
lc_tracers = [
handler
for handler in callback_manager.handlers
if isinstance(handler, LangChainTracer)
]
if lc_tracers:
lc_tracer = lc_tracers[0]
run_tree = run_trees.RunTree(
id=callback_manager.parent_run_id,
session_name=lc_tracer.project_name,
name="Wrapping",
run_type="chain",
inputs={},
tags=callback_manager.tags,
extra={"metadata": callback_manager.metadata},
)
return run_tree
@staticmethod
def _wrap_sync(
func: Callable[..., Output],
) -> Callable[[Input, RunnableConfig], Output]:
"""Wrap a synchronous function to make it asynchronous."""
def wrap_traceable(inputs: dict, config: RunnableConfig) -> Any:
run_tree = RunnableTraceable._configure_run_tree(
config.get("callbacks")
)
return func(**inputs, langsmith_extra={"run_tree": run_tree})
return cast(Callable[[Input, RunnableConfig], Output], wrap_traceable)
@staticmethod
def _wrap_async(
afunc: Optional[Callable[..., Awaitable[Output]]],
) -> Optional[Callable[[Input, RunnableConfig], Awaitable[Output]]]:
"""Wrap an async function to make it synchronous."""
if afunc is None:
return None
if not is_traceable_function(afunc):
raise ValueError(
"RunnableTraceable expects a function wrapped by the LangSmith"
f" @traceable decorator. Got {afunc}"
)
afunc_ = cast(Callable[..., Awaitable[Output]], afunc)
async def awrap_traceable(inputs: dict, config: RunnableConfig) -> Any:
run_tree = RunnableTraceable._configure_run_tree(
config.get("callbacks")
)
return await afunc_(**inputs, langsmith_extra={"run_tree": run_tree})
return cast(
Callable[[Input, RunnableConfig], Awaitable[Output]], awrap_traceable
)
return RunnableTraceable(traceable_fn)