Skip to content

Commit 0ba0adb

Browse files
make cli params optional if specified in .env
1 parent 12b8f2a commit 0ba0adb

File tree

3 files changed

+174
-17
lines changed

3 files changed

+174
-17
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,3 +209,4 @@ __marimo__/
209209
# LangSmith Export Data (may contain sensitive trace data)
210210
langsmith_traces_*.json
211211
traces_export.json
212+
/.idea/

README.md

Lines changed: 54 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,25 @@ Get your API key from: https://smith.langchain.com/settings
7373

7474
## Usage
7575

76-
### Basic Usage
76+
### Option 1: Using Environment Variables (Recommended)
77+
78+
Set up your `.env` file once:
79+
80+
```bash
81+
cp .env.example .env
82+
# Edit .env with your values:
83+
# LANGSMITH_API_KEY=lsv2_pt_your_key_here
84+
# LANGSMITH_PROJECT=your-project-name
85+
# LANGSMITH_LIMIT=150
86+
```
87+
88+
Then use simple commands:
89+
90+
```bash
91+
python export_langsmith_traces.py --output "traces_export.json"
92+
```
93+
94+
### Option 2: Using Command Line Arguments
7795

7896
```bash
7997
python export_langsmith_traces.py \
@@ -83,15 +101,38 @@ python export_langsmith_traces.py \
83101
--output "traces_export.json"
84102
```
85103

104+
### Option 3: Mixed Usage (Override Environment Variables)
105+
106+
```bash
107+
# Override just the project while using env vars for api-key and limit
108+
python export_langsmith_traces.py \
109+
--project "different-project" \
110+
--output "traces_export.json"
111+
```
112+
86113
### Parameters
87114

88-
- `--api-key` (required): LangSmith API key for authentication
89-
- `--project` (required): LangSmith project name or ID
90-
- `--limit` (required): Number of most recent traces to export (must be > 0)
115+
- `--api-key` (optional): LangSmith API key for authentication (default: `LANGSMITH_API_KEY` env var)
116+
- `--project` (optional): LangSmith project name or ID (default: `LANGSMITH_PROJECT` env var)
117+
- `--limit` (optional): Number of most recent traces to export (default: `LANGSMITH_LIMIT` env var)
91118
- `--output` (required): Output JSON file path
92119

93-
### Example
120+
**Note**: While the CLI arguments are now optional, the values must be provided either via command line or environment variables.
121+
122+
### Examples
123+
124+
**Using environment variables:**
125+
```bash
126+
# Set up .env file once
127+
echo "LANGSMITH_API_KEY=lsv2_pt_abc123..." >> .env
128+
echo "LANGSMITH_PROJECT=neota-aesp-project" >> .env
129+
echo "LANGSMITH_LIMIT=200" >> .env
130+
131+
# Simple usage
132+
python export_langsmith_traces.py --output "neota_traces_2025-11-28.json"
133+
```
94134

135+
**Using CLI arguments:**
95136
```bash
96137
python export_langsmith_traces.py \
97138
--api-key "lsv2_pt_abc123..." \
@@ -100,6 +141,14 @@ python export_langsmith_traces.py \
100141
--output "neota_traces_2025-11-28.json"
101142
```
102143

144+
**Mixed usage:**
145+
```bash
146+
# Use env vars for api-key and project, override limit
147+
python export_langsmith_traces.py \
148+
--limit 500 \
149+
--output "large_export.json"
150+
```
151+
103152
## Output Format
104153

105154
The script generates a JSON file with the following structure:

export_langsmith_traces.py

Lines changed: 119 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,13 @@
2222

2323
import argparse
2424
import json
25+
import os
2526
import sys
2627
import time
2728
from datetime import datetime, timezone
2829
from typing import Any, Dict, List
2930

31+
from dotenv import load_dotenv
3032
from langsmith import Client
3133

3234

@@ -87,12 +89,21 @@ def __init__(self, api_key: str, api_url: str = DEFAULT_API_URL) -> None:
8789
f"Please verify your API key is valid. Error: {str(e)}"
8890
) from e
8991

92+
def _looks_like_uuid(self, value: str) -> bool:
93+
"""Check if a string looks like a UUID."""
94+
import re
95+
uuid_pattern = re.compile(
96+
r'^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$',
97+
re.IGNORECASE
98+
)
99+
return bool(uuid_pattern.match(value))
100+
90101
def fetch_runs(self, project_name: str, limit: int) -> List[Any]:
91102
"""
92103
Fetch runs from LangSmith with rate limiting.
93104
94105
Args:
95-
project_name: Name of the LangSmith project
106+
project_name: Name or ID of the LangSmith project
96107
limit: Maximum number of runs to retrieve
97108
98109
Returns:
@@ -103,23 +114,55 @@ def fetch_runs(self, project_name: str, limit: int) -> List[Any]:
103114
RateLimitError: If rate limit exceeded after retries
104115
"""
105116
attempt = 0
117+
last_exception = None
118+
106119
while attempt < self.MAX_RETRIES:
107120
try:
121+
# Try with project_name parameter first
108122
runs = list(
109123
self.client.list_runs(project_name=project_name, limit=limit)
110124
)
111125
return runs
112-
except Exception:
126+
except Exception as e:
127+
last_exception = e
128+
error_msg = str(e).lower()
129+
130+
# Check if this is a project not found error (not a rate limit or network error)
131+
if any(term in error_msg for term in ["not found", "does not exist", "project", "404"]):
132+
# If it looks like a UUID, try as project_id instead
133+
if self._looks_like_uuid(project_name):
134+
print("Trying project ID instead of name...")
135+
try:
136+
runs = list(
137+
self.client.list_runs(project_id=project_name, limit=limit)
138+
)
139+
return runs
140+
except Exception:
141+
pass # Fall through to retry logic
142+
143+
# If first attempt and looks like project name issue, raise specific error
144+
if attempt == 0:
145+
raise ProjectNotFoundError(
146+
f"Project '{project_name}' not found. "
147+
f"Please verify the project name or try using the project ID (UUID format). "
148+
f"You can find the project ID in the LangSmith URL when viewing your project. "
149+
f"Original error: {str(e)}"
150+
) from e
151+
113152
attempt += 1
114153
if attempt >= self.MAX_RETRIES:
115-
raise
154+
break
116155
# Exponential backoff
117156
backoff_time = self.INITIAL_BACKOFF * (
118157
self.BACKOFF_MULTIPLIER ** (attempt - 1)
119158
)
120159
time.sleep(backoff_time)
121-
# This should never be reached, but mypy needs it
122-
raise Exception("Max retries exceeded")
160+
161+
# If we get here, all retries failed
162+
raise RateLimitError(
163+
f"Failed to fetch runs after {self.MAX_RETRIES} attempts. "
164+
f"Last error: {str(last_exception)}"
165+
) from last_exception
123166

124167
def format_trace_data(self, runs: List[Any]) -> Dict[str, Any]:
125168
"""
@@ -225,42 +268,72 @@ def _positive_int(value: str) -> int:
225268
raise argparse.ArgumentTypeError(f"{value} must be an integer")
226269

227270

271+
def _get_env_limit() -> int:
272+
"""Get limit from environment variable with validation."""
273+
try:
274+
limit_str = os.getenv("LANGSMITH_LIMIT", "0")
275+
limit = int(limit_str)
276+
if limit <= 0:
277+
return 0
278+
return limit
279+
except ValueError:
280+
return 0
281+
282+
228283
def parse_arguments() -> argparse.Namespace:
229284
"""
230-
Parse command-line arguments.
285+
Parse command-line arguments with environment variable fallbacks.
231286
232287
Returns:
233288
Parsed arguments namespace
234289
"""
290+
# Load environment variables from .env file
291+
load_dotenv()
292+
235293
parser = argparse.ArgumentParser(
236294
description="Export LangSmith trace data for offline analysis",
237295
formatter_class=argparse.RawDescriptionHelpFormatter,
238296
epilog="""
239-
Example usage:
297+
Example usage with CLI arguments:
240298
python export_langsmith_traces.py \\
241299
--api-key "lsv2_pt_..." \\
242300
--project "my-project" \\
243301
--limit 150 \\
244302
--output "traces_export.json"
303+
304+
Example usage with .env file:
305+
# Set up .env file with defaults
306+
echo "LANGSMITH_API_KEY=lsv2_pt_..." >> .env
307+
echo "LANGSMITH_PROJECT=my-project" >> .env
308+
echo "LANGSMITH_LIMIT=150" >> .env
309+
310+
# Then simple usage
311+
python export_langsmith_traces.py --output traces.json
245312
""",
246313
)
247314

248315
parser.add_argument(
249316
"--api-key",
250317
type=str,
251-
required=True,
252-
help="LangSmith API key for authentication",
318+
required=False,
319+
default=os.getenv("LANGSMITH_API_KEY"),
320+
help="LangSmith API key for authentication (default: LANGSMITH_API_KEY env var)",
253321
)
254322

255323
parser.add_argument(
256-
"--project", type=str, required=True, help="LangSmith project name or ID"
324+
"--project",
325+
type=str,
326+
required=False,
327+
default=os.getenv("LANGSMITH_PROJECT"),
328+
help="LangSmith project name or ID (default: LANGSMITH_PROJECT env var)"
257329
)
258330

259331
parser.add_argument(
260332
"--limit",
261333
type=_positive_int,
262-
required=True,
263-
help="Number of most recent traces to export (must be > 0)",
334+
required=False,
335+
default=_get_env_limit() or None,
336+
help="Number of most recent traces to export (default: LANGSMITH_LIMIT env var)",
264337
)
265338

266339
parser.add_argument(
@@ -270,6 +343,37 @@ def parse_arguments() -> argparse.Namespace:
270343
return parser.parse_args()
271344

272345

346+
def validate_required_args(args: argparse.Namespace) -> None:
347+
"""
348+
Validate that required arguments are provided via CLI or environment.
349+
350+
Args:
351+
args: Parsed command line arguments
352+
353+
Raises:
354+
SystemExit: If required arguments are missing
355+
"""
356+
errors = []
357+
358+
if not args.api_key:
359+
errors.append("--api-key is required (or set LANGSMITH_API_KEY in .env)")
360+
361+
if not args.project:
362+
errors.append("--project is required (or set LANGSMITH_PROJECT in .env)")
363+
364+
if not args.limit:
365+
errors.append("--limit is required (or set LANGSMITH_LIMIT in .env)")
366+
367+
if errors:
368+
print("❌ Missing required arguments:", file=sys.stderr)
369+
for error in errors:
370+
print(f" {error}", file=sys.stderr)
371+
print("\nTip: Create a .env file with your defaults:", file=sys.stderr)
372+
print(" cp .env.example .env", file=sys.stderr)
373+
print(" # Edit .env with your values", file=sys.stderr)
374+
sys.exit(1)
375+
376+
273377
def main() -> None:
274378
"""
275379
Main execution function that orchestrates the export workflow.
@@ -295,6 +399,9 @@ def main() -> None:
295399
try:
296400
# Parse arguments
297401
args = parse_arguments()
402+
403+
# Validate that required arguments are available
404+
validate_required_args(args)
298405

299406
print(f"🚀 Exporting {args.limit} traces from project '{args.project}'...")
300407

0 commit comments

Comments
 (0)