diff --git a/js/package.json b/js/package.json index f4cdc7eaa..cf7d3acfa 100644 --- a/js/package.json +++ b/js/package.json @@ -1,6 +1,6 @@ { "name": "langsmith", - "version": "0.2.4-dev.0", + "version": "0.2.4", "description": "Client library to connect to the LangSmith LLM Tracing and Evaluation Platform.", "packageManager": "yarn@1.22.19", "files": [ diff --git a/js/src/index.ts b/js/src/index.ts index 5a6f0d752..45fc282c0 100644 --- a/js/src/index.ts +++ b/js/src/index.ts @@ -14,4 +14,4 @@ export { RunTree, type RunTreeConfig } from "./run_trees.js"; export { overrideFetchImplementation } from "./singletons/fetch.js"; // Update using yarn bump-version -export const __version__ = "0.2.4-dev.0"; +export const __version__ = "0.2.4"; diff --git a/js/src/tests/vercel.int.test.ts b/js/src/tests/vercel.int.test.ts index 968ec4bdd..b98fbb893 100644 --- a/js/src/tests/vercel.int.test.ts +++ b/js/src/tests/vercel.int.test.ts @@ -52,6 +52,7 @@ test("generateText", async () => { }), }, experimental_telemetry: AISDKExporter.getSettings({ + isEnabled: true, runId, functionId: "functionId", metadata: { userId: "123", language: "english" }, @@ -86,6 +87,7 @@ test("generateText with image", async () => { }, ], experimental_telemetry: AISDKExporter.getSettings({ + isEnabled: true, runId, runName: "vercelImageTest", functionId: "functionId", @@ -125,6 +127,7 @@ test("streamText", async () => { }), }, experimental_telemetry: AISDKExporter.getSettings({ + isEnabled: true, runId, functionId: "functionId", metadata: { userId: "123", language: "english" }, @@ -152,6 +155,7 @@ test("generateObject", async () => { }), prompt: "What's the weather in Prague?", experimental_telemetry: AISDKExporter.getSettings({ + isEnabled: true, runId, functionId: "functionId", metadata: { userId: "123", language: "english" }, @@ -177,6 +181,7 @@ test("streamObject", async () => { }), prompt: "What's the weather in Prague?", experimental_telemetry: AISDKExporter.getSettings({ + isEnabled: true, runId, functionId: "functionId", metadata: { @@ -217,6 +222,7 @@ test("traceable", async () => { }), }, experimental_telemetry: AISDKExporter.getSettings({ + isEnabled: true, functionId: "functionId", runName: "nestedVercelTrace", metadata: { userId: "123", language: "english" }, diff --git a/js/src/tests/vercel.test.ts b/js/src/tests/vercel.test.ts index bc104b9bd..9c7c78fa0 100644 --- a/js/src/tests/vercel.test.ts +++ b/js/src/tests/vercel.test.ts @@ -134,6 +134,7 @@ test("generateText", async () => { }), }, experimental_telemetry: AISDKExporter.getSettings({ + isEnabled: true, runName: "generateText", functionId: "functionId", metadata: { userId: "123", language: "english" }, @@ -354,6 +355,7 @@ test("streamText", async () => { }), }, experimental_telemetry: AISDKExporter.getSettings({ + isEnabled: true, functionId: "functionId", metadata: { userId: "123", language: "english" }, }), @@ -547,6 +549,7 @@ test("generateObject", async () => { }), prompt: "What's the weather in Prague?", experimental_telemetry: AISDKExporter.getSettings({ + isEnabled: true, functionId: "functionId", metadata: { userId: "123", language: "english" }, }), @@ -656,6 +659,7 @@ test("streamObject", async () => { }), prompt: "What's the weather in Prague?", experimental_telemetry: AISDKExporter.getSettings({ + isEnabled: true, functionId: "functionId", metadata: { userId: "123", language: "english" }, }), @@ -755,6 +759,7 @@ test("traceable", async () => { }), }, experimental_telemetry: AISDKExporter.getSettings({ + isEnabled: true, runName: "generateText", functionId: "functionId", metadata: { userId: "123", language: "english" }, diff --git a/js/src/vercel.ts b/js/src/vercel.ts index adc0bce28..c5da1f950 100644 --- a/js/src/vercel.ts +++ b/js/src/vercel.ts @@ -13,6 +13,7 @@ import { getLangSmithEnvironmentVariable, getEnvironmentVariable, } from "./utils/env.js"; +import { isTracingEnabled } from "./env.js"; // eslint-disable-next-line @typescript-eslint/ban-types type AnyString = string & {}; @@ -319,6 +320,8 @@ export class AISDKExporter { this.client = args?.client ?? new Client(); this.debug = args?.debug ?? getEnvironmentVariable("OTEL_LOG_LEVEL") === "DEBUG"; + + this.logDebug("creating exporter", { tracingEnabled: isTracingEnabled() }); } static getSettings(settings?: TelemetrySettings) { @@ -328,7 +331,7 @@ export class AISDKExporter { if (runName != null) metadata[RUN_NAME_METADATA_KEY.input] = runName; // attempt to obtain the run tree if used within a traceable function - let defaultEnabled = true; + let defaultEnabled = settings?.isEnabled ?? isTracingEnabled(); try { const runTree = getCurrentRunTree(); const headers = runTree.toHeaders(); diff --git a/python/langsmith/_internal/_serde.py b/python/langsmith/_internal/_serde.py index 55057920b..e77f7319d 100644 --- a/python/langsmith/_internal/_serde.py +++ b/python/langsmith/_internal/_serde.py @@ -10,9 +10,7 @@ import pathlib import re import uuid -from typing import ( - Any, -) +from typing import Any import orjson @@ -33,14 +31,8 @@ def _simple_default(obj): # https://github.com/ijl/orjson#serialize if isinstance(obj, datetime.datetime): return obj.isoformat() - if isinstance(obj, uuid.UUID): + elif isinstance(obj, uuid.UUID): return str(obj) - if hasattr(obj, "model_dump") and callable(obj.model_dump): - return obj.model_dump() - elif hasattr(obj, "dict") and callable(obj.dict): - return obj.dict() - elif hasattr(obj, "_asdict") and callable(obj._asdict): - return obj._asdict() elif isinstance(obj, BaseException): return {"error": type(obj).__name__, "message": str(obj)} elif isinstance(obj, (set, frozenset, collections.deque)): @@ -77,6 +69,16 @@ def _simple_default(obj): return str(obj) +_serialization_methods = [ + ( + "model_dump", + {"exclude_none": True, "mode": "json"}, + ), # Pydantic V2 with non-serializable fields + ("dict", {}), # Pydantic V1 with non-serializable field + ("to_dict", {}), # dataclasses-json +] + + def _serialize_json(obj: Any) -> Any: try: if isinstance(obj, (set, tuple)): @@ -85,12 +87,7 @@ def _serialize_json(obj: Any) -> Any: return obj._asdict() return list(obj) - serialization_methods = [ - ("model_dump", True), # Pydantic V2 with non-serializable fields - ("dict", False), # Pydantic V1 with non-serializable field - ("to_dict", False), # dataclasses-json - ] - for attr, exclude_none in serialization_methods: + for attr, kwargs in _serialization_methods: if ( hasattr(obj, attr) and callable(getattr(obj, attr)) @@ -98,9 +95,7 @@ def _serialize_json(obj: Any) -> Any: ): try: method = getattr(obj, attr) - response = ( - method(exclude_none=exclude_none) if exclude_none else method() - ) + response = method(**kwargs) if not isinstance(response, dict): return str(response) return response diff --git a/python/langsmith/client.py b/python/langsmith/client.py index 4419a2e34..98ae36c88 100644 --- a/python/langsmith/client.py +++ b/python/langsmith/client.py @@ -137,8 +137,13 @@ def _parse_token_or_url( path_parts = parsed_url.path.split("/") if len(path_parts) >= num_parts: token_uuid = path_parts[-num_parts] + _as_uuid(token_uuid, var="token parts") else: raise ls_utils.LangSmithUserError(f"Invalid public {kind} URL: {url_or_token}") + if parsed_url.netloc == "smith.langchain.com": + api_url = "https://api.smith.langchain.com" + elif parsed_url.netloc == "beta.smith.langchain.com": + api_url = "https://beta.api.smith.langchain.com" return api_url, token_uuid diff --git a/python/langsmith/run_helpers.py b/python/langsmith/run_helpers.py index a687fde66..eaa838192 100644 --- a/python/langsmith/run_helpers.py +++ b/python/langsmith/run_helpers.py @@ -425,10 +425,13 @@ def manual_extra_function(x): 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") + run_type = cast( + 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( diff --git a/python/tests/unit_tests/test_client.py b/python/tests/unit_tests/test_client.py index 9ff1a8eef..7dc505560 100644 --- a/python/tests/unit_tests/test_client.py +++ b/python/tests/unit_tests/test_client.py @@ -7,6 +7,7 @@ import json import logging import math +import pathlib import sys import time import uuid @@ -37,7 +38,9 @@ Client, _dumps_json, _is_langchain_hosted, + _parse_token_or_url, ) +from langsmith.utils import LangSmithUserError _CREATED_AT = datetime(2015, 1, 1, 0, 0, 0) @@ -719,6 +722,7 @@ def test_pydantic_serialize() -> None: class ChildPydantic(BaseModel): uid: uuid.UUID + child_path_keys: Dict[pathlib.Path, pathlib.Path] class MyPydantic(BaseModel): foo: str @@ -726,9 +730,16 @@ class MyPydantic(BaseModel): tim: datetime ex: Optional[str] = None child: Optional[ChildPydantic] = None + path_keys: Dict[pathlib.Path, pathlib.Path] obj = MyPydantic( - foo="bar", uid=test_uuid, tim=test_time, child=ChildPydantic(uid=test_uuid) + foo="bar", + uid=test_uuid, + tim=test_time, + child=ChildPydantic( + uid=test_uuid, child_path_keys={pathlib.Path("foo"): pathlib.Path("bar")} + ), + path_keys={pathlib.Path("foo"): pathlib.Path("bar")}, ) res = json.loads(json.dumps(obj, default=_serialize_json)) expected = { @@ -737,7 +748,9 @@ class MyPydantic(BaseModel): "tim": test_time.isoformat(), "child": { "uid": str(test_uuid), + "child_path_keys": {"foo": "bar"}, }, + "path_keys": {"foo": "bar"}, } assert res == expected @@ -777,6 +790,7 @@ def __repr__(self): class MyPydantic(BaseModel): foo: str bar: int + path_keys: Dict[pathlib.Path, "MyPydantic"] @dataclasses.dataclass class MyDataclass: @@ -816,7 +830,11 @@ class MyNamedTuple(NamedTuple): "class_with_tee": ClassWithTee(), "my_dataclass": MyDataclass("foo", 1), "my_enum": MyEnum.FOO, - "my_pydantic": MyPydantic(foo="foo", bar=1), + "my_pydantic": MyPydantic( + foo="foo", + bar=1, + path_keys={pathlib.Path("foo"): MyPydantic(foo="foo", bar=1, path_keys={})}, + ), "my_pydantic_class": MyPydantic, "person": Person(name="foo_person"), "a_bool": True, @@ -842,7 +860,11 @@ class MyNamedTuple(NamedTuple): "class_with_tee": "tee_a, tee_b", "my_dataclass": {"foo": "foo", "bar": 1}, "my_enum": "foo", - "my_pydantic": {"foo": "foo", "bar": 1}, + "my_pydantic": { + "foo": "foo", + "bar": 1, + "path_keys": {"foo": {"foo": "foo", "bar": 1, "path_keys": {}}}, + }, "my_pydantic_class": lambda x: "MyPydantic" in x, "person": {"name": "foo_person"}, "a_bool": True, @@ -1182,3 +1204,48 @@ def test_validate_api_key_if_hosted( # Check no warning is raised here. warnings.simplefilter("error") client_cls(api_url="http://localhost:1984") + + +def test_parse_token_or_url(): + # Test with URL + url = "https://smith.langchain.com/public/419dcab2-1d66-4b94-8901-0357ead390df/d" + api_url = "https://api.smith.langchain.com" + assert _parse_token_or_url(url, api_url) == ( + api_url, + "419dcab2-1d66-4b94-8901-0357ead390df", + ) + + url = "https://smith.langchain.com/public/419dcab2-1d66-4b94-8901-0357ead390df/d" + beta_api_url = "https://beta.api.smith.langchain.com" + # Should still point to the correct public one + assert _parse_token_or_url(url, beta_api_url) == ( + api_url, + "419dcab2-1d66-4b94-8901-0357ead390df", + ) + + token = "419dcab2-1d66-4b94-8901-0357ead390df" + assert _parse_token_or_url(token, api_url) == ( + api_url, + token, + ) + + # Test with UUID object + token_uuid = uuid.UUID("419dcab2-1d66-4b94-8901-0357ead390df") + assert _parse_token_or_url(token_uuid, api_url) == ( + api_url, + str(token_uuid), + ) + + # Test with custom num_parts + url_custom = ( + "https://smith.langchain.com/public/419dcab2-1d66-4b94-8901-0357ead390df/p/q" + ) + assert _parse_token_or_url(url_custom, api_url, num_parts=3) == ( + api_url, + "419dcab2-1d66-4b94-8901-0357ead390df", + ) + + # Test with invalid URL + invalid_url = "https://invalid.com/419dcab2-1d66-4b94-8901-0357ead390df" + with pytest.raises(LangSmithUserError): + _parse_token_or_url(invalid_url, api_url)