Skip to content

Commit eb08922

Browse files
authored
feat(py): support metadata in target func (#1694)
Add support for accessing example metadata in target ```python def target(inputs: dict, metadata: dict) -> dict: ... ```
1 parent e846b58 commit eb08922

File tree

5 files changed

+126
-126
lines changed

5 files changed

+126
-126
lines changed

python/langsmith/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
from langsmith.utils import ContextThreadPoolExecutor
2121

2222
# Avoid calling into importlib on every call to __version__
23-
__version__ = "0.3.34"
23+
__version__ = "0.3.35"
2424
version = __version__ # for backwards compatibility
2525

2626

python/langsmith/evaluation/_arunner.py

Lines changed: 11 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@
3535
_ExperimentManagerMixin,
3636
_extract_feedback_keys,
3737
_ForwardResults,
38-
_include_attachments,
38+
_get_target_args,
3939
_is_langchain_runnable,
4040
_load_examples_map,
4141
_load_experiment,
@@ -44,6 +44,7 @@
4444
_resolve_data,
4545
_resolve_evaluators,
4646
_resolve_experiment,
47+
_target_include_attachments,
4748
_to_pandas,
4849
_wrap_summary_evaluators,
4950
)
@@ -464,6 +465,9 @@ async def _aevaluate(
464465
runs,
465466
client,
466467
)
468+
num_include_attachments = int(
469+
_target_include_attachments(target)
470+
) + _evaluators_include_attachments(evaluators)
467471
manager = await _AsyncExperimentManager(
468472
data,
469473
client=client,
@@ -472,14 +476,8 @@ async def _aevaluate(
472476
description=description,
473477
num_repetitions=num_repetitions,
474478
runs=runs,
475-
include_attachments=_include_attachments(target)
476-
or _evaluators_include_attachments(evaluators) > 0,
477-
reuse_attachments=num_repetitions
478-
* (
479-
int(_include_attachments(target))
480-
+ _evaluators_include_attachments(evaluators)
481-
)
482-
> 1,
479+
include_attachments=num_include_attachments > 0,
480+
reuse_attachments=num_repetitions * num_include_attachments > 1,
483481
upload_results=upload_results,
484482
).astart()
485483
cache_dir = ls_utils.get_cache_dir(None)
@@ -785,7 +783,7 @@ async def process_example(example: schemas.Example):
785783
self.experiment_name,
786784
self._metadata,
787785
self.client,
788-
_include_attachments(target),
786+
_target_include_attachments(target),
789787
)
790788
example, run = pred["example"], pred["run"]
791789
result = await self._arun_evaluators(
@@ -842,7 +840,7 @@ async def awith_predictions(
842840
_experiment_results = self._apredict(
843841
target,
844842
max_concurrency=max_concurrency,
845-
include_attachments=_include_attachments(target),
843+
include_attachments=_target_include_attachments(target),
846844
)
847845
r1, r2 = aitertools.atee(_experiment_results, 2, lock=asyncio.Lock())
848846
return _AsyncExperimentManager(
@@ -1236,11 +1234,8 @@ def _get_run(r: run_trees.RunTree) -> None:
12361234

12371235
with rh.tracing_context(enabled=True):
12381236
try:
1239-
args = (
1240-
(example.inputs, example.attachments)
1241-
if include_attachments
1242-
else (example.inputs,)
1243-
)
1237+
arg_names = _get_target_args(fn)
1238+
args = [getattr(example, argn) for argn in arg_names]
12441239
await fn(
12451240
*args,
12461241
langsmith_extra=rh.LangSmithExtra(

python/langsmith/evaluation/_runner.py

Lines changed: 52 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1055,8 +1055,7 @@ def _evaluate(
10551055
# If provided, we don't need to create a new experiment.
10561056
runs=runs,
10571057
# Create or resolve the experiment.
1058-
include_attachments=_include_attachments(target)
1059-
or _evaluators_include_attachments(evaluators) > 0,
1058+
include_attachments=_include_attachments(target, evaluators),
10601059
upload_results=upload_results,
10611060
).start()
10621061
cache_dir = ls_utils.get_cache_dir(None)
@@ -1459,7 +1458,7 @@ def with_predictions(
14591458
self._predict,
14601459
target,
14611460
max_concurrency=max_concurrency,
1462-
include_attachments=_include_attachments(target),
1461+
include_attachments=_target_include_attachments(target),
14631462
)
14641463
r1, r2 = itertools.tee(_experiment_results, 2)
14651464
return _ExperimentManager(
@@ -1901,15 +1900,10 @@ def _get_run(r: rt.RunTree) -> None:
19011900
client=client,
19021901
)
19031902
try:
1904-
args = (
1905-
(example.inputs, example.attachments)
1906-
if include_attachments
1907-
else (example.inputs,)
1908-
)
1909-
fn(
1910-
*args,
1911-
langsmith_extra=langsmith_extra,
1912-
)
1903+
arg_names = _get_target_args(fn)
1904+
args = [getattr(example, argn) for argn in arg_names]
1905+
fn(*args, langsmith_extra=langsmith_extra)
1906+
# Reset attachment readers if attachments were used.
19131907
if include_attachments and example.attachments is not None:
19141908
for attachment in example.attachments:
19151909
reader = example.attachments[attachment]["reader"]
@@ -1981,31 +1975,41 @@ def _ensure_traceable(
19811975
return fn
19821976

19831977

1984-
def _evaluators_include_attachments(
1985-
evaluators: Optional[Sequence[Union[EVALUATOR_T, AEVALUATOR_T]]],
1986-
) -> int:
1978+
def _include_attachments(target: Any, evaluators: Optional[Sequence]) -> bool:
1979+
return _target_include_attachments(target) or bool(
1980+
_evaluators_include_attachments(evaluators)
1981+
)
1982+
1983+
1984+
def _evaluators_include_attachments(evaluators: Optional[Sequence]) -> int:
19871985
if evaluators is None:
19881986
return 0
19891987

1990-
def evaluator_uses_attachments(evaluator: Any) -> bool:
1991-
if not callable(evaluator):
1992-
return False
1993-
sig = inspect.signature(evaluator)
1994-
params = list(sig.parameters.values())
1995-
positional_params = [
1996-
p for p in params if p.kind in (p.POSITIONAL_ONLY, p.POSITIONAL_OR_KEYWORD)
1997-
]
1998-
return any(p.name == "attachments" for p in positional_params)
1988+
return sum(_evaluator_uses_attachments(e) for e in evaluators)
1989+
1990+
1991+
def _evaluator_uses_attachments(evaluator: Any) -> bool:
1992+
if not callable(evaluator):
1993+
return False
1994+
sig = inspect.signature(evaluator)
1995+
params = list(sig.parameters.values())
1996+
positional_params = [
1997+
p for p in params if p.kind in (p.POSITIONAL_ONLY, p.POSITIONAL_OR_KEYWORD)
1998+
]
1999+
return any(p.name == "attachments" for p in positional_params)
2000+
19992001

2000-
return sum(evaluator_uses_attachments(e) for e in evaluators)
2002+
def _target_include_attachments(target: Any) -> bool:
2003+
"""Whether the target function accepts attachments."""
2004+
return "attachments" in _get_target_args(target)
20012005

20022006

2003-
def _include_attachments(
2004-
target: Any,
2005-
) -> bool:
2007+
def _get_target_args(target: Any) -> list[str]:
20062008
"""Whether the target function accepts attachments."""
2007-
if _is_langchain_runnable(target) or not callable(target):
2008-
return False
2009+
if not callable(target):
2010+
return []
2011+
if _is_langchain_runnable(target):
2012+
return ["inputs"]
20092013
# Check function signature
20102014
sig = inspect.signature(target)
20112015
params = list(sig.parameters.values())
@@ -2018,21 +2022,27 @@ def _include_attachments(
20182022
raise ValueError(
20192023
"Target function must accept at least one positional argument (inputs)."
20202024
)
2021-
elif len(positional_no_default) > 2:
2025+
elif len(positional_no_default) > 3:
20222026
raise ValueError(
2023-
"Target function must accept at most two "
2024-
"arguments without default values: (inputs, attachments)."
2027+
"Target function must accept at most three "
2028+
"arguments without default values: (inputs, attachments, metadata)."
2029+
)
2030+
elif len(positional_no_default) > 1 and {
2031+
p.name for p in positional_no_default
2032+
}.difference(["inputs", "attachments", "metadata"]):
2033+
raise ValueError(
2034+
"When passing multiple positional arguments without default values, they "
2035+
"must be named 'inputs', 'attachments', or 'metadata'. Received: "
2036+
f"{[p.name for p in positional_no_default]}"
20252037
)
2026-
elif len(positional_no_default) == 2:
2027-
if [p.name for p in positional_no_default] != ["inputs", "attachments"]:
2028-
raise ValueError(
2029-
"When passing 2 positional arguments, they must be named "
2030-
"'inputs' and 'attachments', respectively. Received: "
2031-
f"{[p.name for p in positional_no_default]}"
2032-
)
2033-
return True
20342038
else:
2035-
return [p.name for p in positional_params[:2]] == ["inputs", "attachments"]
2039+
args = []
2040+
for p in positional_params[:3]:
2041+
if p.name in {"inputs", "attachments", "metadata"}:
2042+
args.append(p.name)
2043+
else:
2044+
break
2045+
return args or ["inputs"]
20362046

20372047

20382048
def _resolve_experiment(

python/pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[tool.poetry]
22
name = "langsmith"
3-
version = "0.3.34"
3+
version = "0.3.35"
44
description = "Client library to connect to the LangSmith LLM Tracing and Evaluation Platform."
55
authors = ["LangChain <[email protected]>"]
66
license = "MIT"

0 commit comments

Comments
 (0)