2222
2323import argparse
2424import json
25+ import os
2526import sys
2627import time
2728from datetime import datetime , timezone
2829from typing import Any , Dict , List
2930
31+ from dotenv import load_dotenv
3032from 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+
228283def 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 ("\n Tip: 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+
273377def 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