Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 8 additions & 7 deletions src/langsmith_cli/fetchers.py
Original file line number Diff line number Diff line change
Expand Up @@ -412,7 +412,7 @@ def _fetch_traces_concurrent(
# Batch fetch feedback for all runs that have it
if include_metadata and include_feedback and runs_with_feedback:
feedback_start = perf_counter()
feedback_map = _fetch_feedback_batch(runs_with_feedback, api_key, max_workers)
feedback_map = _fetch_feedback_batch(runs_with_feedback, base_url, api_key, max_workers)
timing_info["feedback_duration"] = perf_counter() - feedback_start

# Add feedback to corresponding traces
Expand Down Expand Up @@ -703,7 +703,7 @@ def _serialize_feedback(fb) -> dict[str, Any]:
}


def _fetch_feedback(run_id: str, *, api_key: str) -> list[dict[str, Any]]:
def _fetch_feedback(run_id: str, *, base_url: str, api_key: str) -> list[dict[str, Any]]:
"""Fetch full feedback objects for a single run.

Args:
Expand All @@ -719,7 +719,7 @@ def _fetch_feedback(run_id: str, *, api_key: str) -> list[dict[str, Any]]:
from langsmith import Client

try:
client = Client(api_key=api_key)
client = Client(api_url=base_url, api_key=api_key)
feedback_list = list(client.list_feedback(run_id=run_id))
return [_serialize_feedback(fb) for fb in feedback_list]
except Exception as e:
Expand All @@ -729,6 +729,7 @@ def _fetch_feedback(run_id: str, *, api_key: str) -> list[dict[str, Any]]:

def _fetch_feedback_batch(
run_ids: list[str],
base_url: str,
api_key: str,
max_workers: int = 5,
) -> dict[str, list[dict[str, Any]]]:
Expand All @@ -748,7 +749,7 @@ def _fetch_feedback_batch(
def fetch_single(run_id: str) -> tuple[str, list[dict[str, Any]]]:
"""Fetch feedback for a single run with error handling."""
try:
feedback = _fetch_feedback(run_id, api_key=api_key)
feedback = _fetch_feedback(run_id, base_url=base_url, api_key=api_key)
return run_id, feedback
except Exception:
return run_id, []
Expand Down Expand Up @@ -811,7 +812,7 @@ def fetch_trace_with_metadata(
# Fetch feedback if requested and feedback exists
feedback = []
if include_feedback and _has_feedback(metadata):
feedback = _fetch_feedback(trace_id, api_key=api_key)
feedback = _fetch_feedback(trace_id, base_url=base_url, api_key=api_key)

return {
"trace_id": trace_id,
Expand Down Expand Up @@ -859,7 +860,7 @@ def fetch_thread_with_metadata(
try:
from langsmith import Client

client = Client(api_key=api_key)
client = Client(api_url=base_url, api_key=api_key)

# Query for root runs with this thread_id (most recent first)
runs = list(
Expand All @@ -876,7 +877,7 @@ def fetch_thread_with_metadata(

# Fetch feedback if requested and feedback exists
if include_feedback and _sdk_run_has_feedback(root_run):
feedback = _fetch_feedback(str(root_run.id), api_key=api_key)
feedback = _fetch_feedback(str(root_run.id), base_url=base_url, api_key=api_key)

except Exception as e:
print(
Expand Down
149 changes: 149 additions & 0 deletions tests/test_fetchers.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import json
from datetime import datetime
from typing import assert_type
from unittest.mock import Mock, patch

import pytest
Expand Down Expand Up @@ -793,3 +794,151 @@ def test_fetch_recent_threads_with_since(self, sample_thread_response):
request_body = json.loads(responses.calls[0].request.body)
assert "start_time" in request_body
assert len(results) == 1


class TestFetchThreadWithMetadata:
"""Tests for fetch_thread_with_metadata function."""

@responses.activate
@patch("langsmith.Client")
def test_fetch_thread_with_metadata_success(
self, mock_client_class, sample_thread_response
):
"""Test successful thread fetching with metadata."""
responses.add(
responses.GET,
f"https://api.smith.langchain.com/runs/threads/{TEST_THREAD_ID}",
json=sample_thread_response,
status=200,
)

mock_client = Mock()
mock_run = Mock()
mock_run.id = "run-123"
mock_run.status = "success"
mock_run.start_time = None
mock_run.end_time = None
mock_run.extra = {}
mock_run.feedback_stats = {}
mock_client.list_runs.return_value = [mock_run]
mock_client_class.return_value = mock_client

result = fetchers.fetch_thread_with_metadata(
TEST_THREAD_ID,
TEST_PROJECT_UUID,
base_url=TEST_BASE_URL,
api_key=TEST_API_KEY,
include_feedback=False,
)

assert result["thread_id"] == TEST_THREAD_ID
assert len(result["messages"]) == 3
assert result["metadata"]["status"] == "success"
assert result["feedback"] == []

@responses.activate
@patch("langsmith.Client")
def test_fetch_thread_with_metadata_respects_base_url(
self, mock_client_class, sample_thread_response
):
"""Test that fetch_thread_with_metadata passes base_url to Client constructor."""
custom_base_url = "https://custom.langsmith.api/v1"

responses.add(
responses.GET,
f"{custom_base_url}/runs/threads/{TEST_THREAD_ID}",
json=sample_thread_response,
status=200,
)

mock_client = Mock()
mock_client.list_runs.return_value = []
mock_client_class.return_value = mock_client

fetchers.fetch_thread_with_metadata(
TEST_THREAD_ID,
TEST_PROJECT_UUID,
base_url=custom_base_url,
api_key=TEST_API_KEY,
include_feedback=False,
)

mock_client_class.assert_called_once_with(
api_url=custom_base_url, api_key=TEST_API_KEY
)

@responses.activate
@patch("langsmith.Client")
@patch("langsmith_cli.fetchers._fetch_feedback")
def test_fetch_thread_with_metadata_with_feedback(
self, mock_fetch_feedback, mock_client_class, sample_thread_response
):
"""Test that fetch_thread_with_metadata fetches feedback with correct base_url."""
custom_base_url = "https://custom.langsmith.api/v1"

responses.add(
responses.GET,
f"{custom_base_url}/runs/threads/{TEST_THREAD_ID}",
json=sample_thread_response,
status=200,
)

# Mock a run with feedback
mock_client = Mock()
mock_run = Mock()
mock_run.id = "run-123"
mock_run.status = "success"
mock_run.start_time = None
mock_run.end_time = None
mock_run.extra = {}
mock_run.feedback_stats = {"thumbs_up": 1} # Has feedback
mock_client.list_runs.return_value = [mock_run]
mock_client_class.return_value = mock_client
mock_fetch_feedback.return_value = [{"key": "thumbs_up", "score": 1}]

result = fetchers.fetch_thread_with_metadata(
TEST_THREAD_ID,
TEST_PROJECT_UUID,
base_url=custom_base_url,
api_key=TEST_API_KEY,
include_feedback=True,
)

# Verify _fetch_feedback was called with the correct base_url
mock_fetch_feedback.assert_called_once_with(
"run-123", base_url=custom_base_url, api_key=TEST_API_KEY
)

# Verify feedback was included in result
assert len(result["feedback"]) == 1
assert result["feedback"][0]["key"] == "thumbs_up"

@responses.activate
@patch("langsmith.Client")
def test_fetch_thread_with_metadata_without_langsmith(
self, mock_client_class, sample_thread_response
):
"""Test thread fetching when metadata fetch fails gracefully."""
responses.add(
responses.GET,
f"https://api.smith.langchain.com/runs/threads/{TEST_THREAD_ID}",
json=sample_thread_response,
status=200,
)

mock_client = Mock()
mock_client.list_runs.side_effect = Exception("API error")
mock_client_class.return_value = mock_client

result = fetchers.fetch_thread_with_metadata(
TEST_THREAD_ID,
TEST_PROJECT_UUID,
base_url=TEST_BASE_URL,
api_key=TEST_API_KEY,
include_feedback=False,
)

assert result["thread_id"] == TEST_THREAD_ID
assert len(result["messages"]) == 3
assert result["metadata"] == {}
assert result["feedback"] == []