langsmith-cli 0.1.3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
File without changes
@@ -0,0 +1,29 @@
1
+ import click
2
+ import os
3
+ import webbrowser
4
+ from rich.console import Console
5
+
6
+ console = Console()
7
+
8
+
9
+ @click.command()
10
+ def login():
11
+ """
12
+ Configure LangSmith API Key.
13
+ """
14
+ url = "https://smith.langchain.com/settings"
15
+ click.echo(f"Opening LangSmith settings to retrieve your API Key: {url}")
16
+ webbrowser.open(url)
17
+ api_key = click.prompt("Enter your LangSmith API Key", hide_input=True)
18
+
19
+ env_file = ".env"
20
+
21
+ if os.path.exists(env_file):
22
+ if not click.confirm(f"{env_file} already exists. Overwrite?", default=False):
23
+ console.print("[yellow]Aborted.[/yellow]")
24
+ return
25
+
26
+ with open(env_file, "w") as f:
27
+ f.write(f"LANGSMITH_API_KEY={api_key}\n")
28
+
29
+ console.print(f"[green]Successfully logged in![/green] API key saved to {env_file}")
@@ -0,0 +1,170 @@
1
+ import click
2
+ from rich.console import Console
3
+ from rich.table import Table
4
+ import langsmith
5
+ import json
6
+ import os
7
+ from langsmith_cli.utils import (
8
+ print_empty_result_message,
9
+ parse_json_string,
10
+ parse_comma_separated_list,
11
+ )
12
+
13
+ console = Console()
14
+
15
+
16
+ @click.group()
17
+ def datasets():
18
+ """Manage LangSmith datasets."""
19
+ pass
20
+
21
+
22
+ @datasets.command("list")
23
+ @click.option("--dataset-ids", help="Specific dataset IDs (comma-separated).")
24
+ @click.option("--limit", default=20, help="Limit number of datasets (default 20).")
25
+ @click.option("--data-type", help="Filter by dataset type (kv, chat, llm).")
26
+ @click.option("--name", "dataset_name", help="Exact dataset name match.")
27
+ @click.option("--name-contains", help="Dataset name substring search.")
28
+ @click.option("--metadata", help="Filter by metadata (JSON string).")
29
+ @click.pass_context
30
+ def list_datasets(
31
+ ctx, dataset_ids, limit, data_type, dataset_name, name_contains, metadata
32
+ ):
33
+ """List all available datasets."""
34
+ client = langsmith.Client()
35
+
36
+ # Parse comma-separated dataset IDs
37
+ dataset_ids_list = parse_comma_separated_list(dataset_ids)
38
+
39
+ # Parse metadata JSON
40
+ metadata_dict = parse_json_string(metadata, "metadata")
41
+
42
+ datasets_gen = client.list_datasets(
43
+ dataset_ids=dataset_ids_list
44
+ if dataset_ids_list is None
45
+ else list(dataset_ids_list), # type: ignore[arg-type]
46
+ limit=limit,
47
+ data_type=data_type,
48
+ dataset_name=dataset_name,
49
+ dataset_name_contains=name_contains,
50
+ metadata=metadata_dict,
51
+ )
52
+ datasets_list = list(datasets_gen)
53
+
54
+ if ctx.obj.get("json"):
55
+ # Use SDK's Pydantic models with focused field selection for context efficiency
56
+ data = [
57
+ d.model_dump(
58
+ include={
59
+ "id",
60
+ "name",
61
+ "inputs_schema",
62
+ "outputs_schema",
63
+ "description",
64
+ "data_type",
65
+ "example_count",
66
+ "session_count",
67
+ "created_at",
68
+ "modified_at",
69
+ "last_session_start_time",
70
+ },
71
+ mode="json",
72
+ )
73
+ for d in datasets_list
74
+ ]
75
+ click.echo(json.dumps(data, default=str))
76
+ return
77
+
78
+ table = Table(title="Datasets")
79
+ table.add_column("Name", style="cyan")
80
+ table.add_column("ID", style="dim")
81
+ table.add_column("Type")
82
+
83
+ for d in datasets_list:
84
+ table.add_row(
85
+ d.name,
86
+ str(d.id),
87
+ d.data_type,
88
+ )
89
+
90
+ if not datasets_list:
91
+ print_empty_result_message(console, "datasets")
92
+ else:
93
+ console.print(table)
94
+
95
+
96
+ @datasets.command("get")
97
+ @click.argument("dataset_id")
98
+ @click.pass_context
99
+ def get_dataset(ctx, dataset_id):
100
+ """Fetch details of a single dataset."""
101
+ client = langsmith.Client()
102
+ dataset = client.read_dataset(dataset_id=dataset_id)
103
+
104
+ data = dataset.dict() if hasattr(dataset, "dict") else dict(dataset)
105
+
106
+ if ctx.obj.get("json"):
107
+ click.echo(json.dumps(data, default=str))
108
+ return
109
+
110
+ console.print(f"[bold]Name:[/bold] {data.get('name')}")
111
+ console.print(f"[bold]ID:[/bold] {data.get('id')}")
112
+ console.print(f"[bold]Description:[/bold] {data.get('description')}")
113
+
114
+
115
+ @datasets.command("create")
116
+ @click.argument("name")
117
+ @click.option("--description", help="Dataset description.")
118
+ @click.option(
119
+ "--type", "dataset_type", default="kv", help="Dataset type (kv, chat, etc.)"
120
+ )
121
+ @click.pass_context
122
+ def create_dataset(ctx, name, description, dataset_type):
123
+ """Create a new dataset."""
124
+ client = langsmith.Client()
125
+ dataset = client.create_dataset(
126
+ dataset_name=name, description=description, data_type=dataset_type
127
+ )
128
+
129
+ if ctx.obj.get("json"):
130
+ data = dataset.dict() if hasattr(dataset, "dict") else dict(dataset)
131
+ click.echo(json.dumps(data, default=str))
132
+ return
133
+
134
+ console.print(f"[green]Created dataset {dataset.name}[/green] (ID: {dataset.id})")
135
+
136
+
137
+ @datasets.command("push")
138
+ @click.argument("file_path", type=click.Path(exists=True))
139
+ @click.option("--dataset", help="Dataset name to push to. Created if not exists.")
140
+ @click.pass_context
141
+ def push_dataset(ctx, file_path, dataset):
142
+ """Upload examples from a JSONL file to a dataset."""
143
+ client = langsmith.Client()
144
+
145
+ if not dataset:
146
+ dataset = os.path.basename(file_path).split(".")[0]
147
+
148
+ # Create dataset if not exists (simple check)
149
+ try:
150
+ client.read_dataset(dataset_name=dataset)
151
+ except Exception:
152
+ console.print(f"[yellow]Dataset '{dataset}' not found. Creating it...[/yellow]")
153
+ client.create_dataset(dataset_name=dataset)
154
+
155
+ examples = []
156
+ with open(file_path, "r") as f:
157
+ for line in f:
158
+ if line.strip():
159
+ examples.append(json.loads(line))
160
+
161
+ # Expecting examples in [{"inputs": {...}, "outputs": {...}}, ...] format
162
+ client.create_examples(
163
+ inputs=[e.get("inputs", {}) for e in examples],
164
+ outputs=[e.get("outputs") for e in examples],
165
+ dataset_name=dataset,
166
+ )
167
+
168
+ console.print(
169
+ f"[green]Successfully pushed {len(examples)} examples to dataset '{dataset}'[/green]"
170
+ )
@@ -0,0 +1,170 @@
1
+ import click
2
+ from rich.console import Console
3
+ from rich.table import Table
4
+ import langsmith
5
+ import json
6
+ from langsmith_cli.utils import (
7
+ print_empty_result_message,
8
+ parse_json_string,
9
+ parse_comma_separated_list,
10
+ )
11
+
12
+ console = Console()
13
+
14
+
15
+ @click.group()
16
+ def examples():
17
+ """Manage dataset examples."""
18
+ pass
19
+
20
+
21
+ @examples.command("list")
22
+ @click.option("--dataset", help="Dataset ID or Name.")
23
+ @click.option("--example-ids", help="Specific example IDs (comma-separated).")
24
+ @click.option("--limit", default=20, help="Limit number of examples (default 20).")
25
+ @click.option("--offset", default=0, help="Number of examples to skip (pagination).")
26
+ @click.option("--filter", "filter_", help="LangSmith query filter.")
27
+ @click.option("--metadata", help="Filter by metadata (JSON string).")
28
+ @click.option("--splits", help="Filter by dataset splits (comma-separated).")
29
+ @click.option("--inline-s3-urls", type=bool, help="Include S3 URLs inline.")
30
+ @click.option("--include-attachments", type=bool, help="Include attachments.")
31
+ @click.option("--as-of", help="Dataset version tag or ISO timestamp.")
32
+ @click.pass_context
33
+ def list_examples(
34
+ ctx,
35
+ dataset,
36
+ example_ids,
37
+ limit,
38
+ offset,
39
+ filter_,
40
+ metadata,
41
+ splits,
42
+ inline_s3_urls,
43
+ include_attachments,
44
+ as_of,
45
+ ):
46
+ """List examples for a dataset."""
47
+ client = langsmith.Client()
48
+
49
+ # Parse comma-separated values
50
+ example_ids_list = parse_comma_separated_list(example_ids)
51
+ splits_list = parse_comma_separated_list(splits)
52
+ metadata_dict = parse_json_string(metadata, "metadata")
53
+
54
+ # list_examples takes dataset_name and limit
55
+ examples_gen = client.list_examples(
56
+ dataset_name=dataset,
57
+ example_ids=example_ids_list,
58
+ limit=limit,
59
+ offset=offset,
60
+ filter=filter_,
61
+ metadata=metadata_dict,
62
+ splits=splits_list,
63
+ inline_s3_urls=inline_s3_urls,
64
+ include_attachments=include_attachments,
65
+ as_of=as_of,
66
+ )
67
+ examples_list = list(examples_gen)
68
+
69
+ if ctx.obj.get("json"):
70
+ # Use SDK's Pydantic models with focused field selection for context efficiency
71
+ data = [
72
+ e.model_dump(
73
+ include={
74
+ "id",
75
+ "inputs",
76
+ "outputs",
77
+ "metadata",
78
+ "dataset_id",
79
+ "created_at",
80
+ "modified_at",
81
+ },
82
+ mode="json",
83
+ )
84
+ for e in examples_list
85
+ ]
86
+ click.echo(json.dumps(data, default=str))
87
+ return
88
+
89
+ table = Table(title=f"Examples: {dataset}")
90
+ table.add_column("ID", style="dim")
91
+ table.add_column("Inputs")
92
+ table.add_column("Outputs")
93
+
94
+ for e in examples_list:
95
+ inputs = json.dumps(e.inputs)
96
+ outputs = json.dumps(e.outputs)
97
+ # Truncate for table
98
+ if len(inputs) > 50:
99
+ inputs = inputs[:47] + "..."
100
+ if len(outputs) > 50:
101
+ outputs = outputs[:47] + "..."
102
+
103
+ table.add_row(str(e.id), inputs, outputs)
104
+
105
+ if not examples_list:
106
+ print_empty_result_message(console, "examples")
107
+ else:
108
+ console.print(table)
109
+
110
+
111
+ @examples.command("get")
112
+ @click.argument("example_id")
113
+ @click.option("--as-of", help="Dataset version tag or ISO timestamp.")
114
+ @click.pass_context
115
+ def get_example(ctx, example_id, as_of):
116
+ """Fetch details of a single example."""
117
+ client = langsmith.Client()
118
+ example = client.read_example(example_id, as_of=as_of)
119
+
120
+ data = example.dict() if hasattr(example, "dict") else dict(example)
121
+
122
+ if ctx.obj.get("json"):
123
+ click.echo(json.dumps(data, default=str))
124
+ return
125
+
126
+ from rich.syntax import Syntax
127
+
128
+ console.print(f"[bold]Example ID:[/bold] {data.get('id')}")
129
+ console.print("\n[bold]Inputs:[/bold]")
130
+ console.print(Syntax(json.dumps(data.get("inputs"), indent=2), "json"))
131
+ console.print("\n[bold]Outputs:[/bold]")
132
+ console.print(Syntax(json.dumps(data.get("outputs"), indent=2), "json"))
133
+
134
+
135
+ @examples.command("create")
136
+ @click.option("--dataset", required=True, help="Dataset ID or Name.")
137
+ @click.option("--inputs", required=True, help="JSON string of inputs.")
138
+ @click.option("--outputs", help="JSON string of outputs.")
139
+ @click.option("--metadata", help="JSON string of metadata.")
140
+ @click.option("--split", help="Dataset split (e.g., train, test, validation).")
141
+ @click.pass_context
142
+ def create_example(ctx, dataset, inputs, outputs, metadata, split):
143
+ """Create a new example in a dataset."""
144
+ client = langsmith.Client()
145
+
146
+ input_dict = parse_json_string(inputs, "inputs")
147
+ output_dict = parse_json_string(outputs, "outputs")
148
+ metadata_dict = parse_json_string(metadata, "metadata")
149
+
150
+ # Handle split - can be a single string or list
151
+ split_value = None
152
+ if split:
153
+ split_value = [split] if isinstance(split, str) else split
154
+
155
+ example = client.create_example(
156
+ inputs=input_dict,
157
+ outputs=output_dict,
158
+ dataset_name=dataset,
159
+ metadata=metadata_dict,
160
+ split=split_value,
161
+ )
162
+
163
+ if ctx.obj.get("json"):
164
+ data = example.dict() if hasattr(example, "dict") else dict(example)
165
+ click.echo(json.dumps(data, default=str))
166
+ return
167
+
168
+ console.print(
169
+ f"[green]Created example[/green] (ID: {example.id}) in dataset {dataset}"
170
+ )
@@ -0,0 +1,162 @@
1
+ import click
2
+ from rich.console import Console
3
+ from rich.table import Table
4
+ import langsmith
5
+ import json
6
+ from langsmith_cli.utils import (
7
+ output_formatted_data,
8
+ sort_items,
9
+ apply_regex_filter,
10
+ apply_wildcard_filter,
11
+ determine_output_format,
12
+ )
13
+
14
+ console = Console()
15
+
16
+
17
+ @click.group()
18
+ def projects():
19
+ """Manage LangSmith projects."""
20
+ pass
21
+
22
+
23
+ @projects.command("list")
24
+ @click.option("--limit", default=100, help="Limit number of projects (default 100).")
25
+ @click.option("--name", "name_", help="Filter by project name substring.")
26
+ @click.option("--name-pattern", help="Filter by name with wildcards (e.g. '*prod*').")
27
+ @click.option(
28
+ "--name-regex", help="Filter by name with regex (e.g. '^prod-.*-v[0-9]+$')."
29
+ )
30
+ @click.option(
31
+ "--reference-dataset-id", help="Filter experiments for a dataset (by ID)."
32
+ )
33
+ @click.option(
34
+ "--reference-dataset-name", help="Filter experiments for a dataset (by name)."
35
+ )
36
+ @click.option(
37
+ "--has-runs", is_flag=True, help="Show only projects with runs (run_count > 0)."
38
+ )
39
+ @click.option(
40
+ "--sort-by", help="Sort by field (name, run_count). Prefix with - for descending."
41
+ )
42
+ @click.option(
43
+ "--format",
44
+ "output_format",
45
+ type=click.Choice(["table", "json", "csv", "yaml"]),
46
+ help="Output format (default: table, or json if --json flag used).",
47
+ )
48
+ @click.pass_context
49
+ def list_projects(
50
+ ctx,
51
+ limit,
52
+ name_,
53
+ name_pattern,
54
+ name_regex,
55
+ reference_dataset_id,
56
+ reference_dataset_name,
57
+ has_runs,
58
+ sort_by,
59
+ output_format,
60
+ ):
61
+ """List all projects."""
62
+
63
+ client = langsmith.Client()
64
+
65
+ # Use name_ (SDK substring filter) if provided and no pattern/regex
66
+ # Use name_pattern as a fallback to name_ if neither are specific filters
67
+ api_name_filter = name_
68
+ if name_pattern and not name_:
69
+ # Extract search term from wildcard pattern for API filtering
70
+ search_term = name_pattern.replace("*", "")
71
+ if search_term:
72
+ api_name_filter = search_term
73
+
74
+ # list_projects returns a generator
75
+ projects_gen = client.list_projects(
76
+ limit=limit,
77
+ name=api_name_filter,
78
+ reference_dataset_id=reference_dataset_id,
79
+ reference_dataset_name=reference_dataset_name,
80
+ )
81
+
82
+ # Materialize the list to count and process
83
+ projects_list = list(projects_gen)
84
+
85
+ # Client-side pattern matching (wildcards)
86
+ projects_list = apply_wildcard_filter(projects_list, name_pattern, lambda p: p.name)
87
+
88
+ # Client-side regex filtering
89
+ projects_list = apply_regex_filter(projects_list, name_regex, lambda p: p.name)
90
+
91
+ # Filter by projects with runs
92
+ if has_runs:
93
+ projects_list = [
94
+ p
95
+ for p in projects_list
96
+ if hasattr(p, "run_count") and p.run_count and p.run_count > 0
97
+ ]
98
+
99
+ # Client-side sorting for table output
100
+ if sort_by and not ctx.obj.get("json"):
101
+ # Map sort field to project attribute
102
+ sort_key_map = {
103
+ "name": lambda p: (p.name or "").lower(),
104
+ "run_count": lambda p: p.run_count
105
+ if hasattr(p, "run_count") and p.run_count
106
+ else 0,
107
+ }
108
+ projects_list = sort_items(projects_list, sort_by, sort_key_map, console)
109
+
110
+ # Determine output format
111
+ format_type = determine_output_format(output_format, ctx.obj.get("json"))
112
+
113
+ # Handle non-table formats
114
+ if format_type != "table":
115
+ # Use SDK's Pydantic models with focused field selection for context efficiency
116
+ data = [
117
+ p.model_dump(
118
+ include={"name", "id"},
119
+ mode="json",
120
+ )
121
+ for p in projects_list
122
+ ]
123
+ output_formatted_data(data, format_type)
124
+ return
125
+
126
+ table = Table(title="Projects")
127
+ table.add_column("Name", style="cyan")
128
+ table.add_column("ID", style="dim")
129
+
130
+ for p in projects_list:
131
+ # Access attributes directly (type-safe)
132
+ table.add_row(p.name, str(p.id))
133
+
134
+ if not projects_list:
135
+ console.print("[yellow]No projects found.[/yellow]")
136
+ else:
137
+ console.print(table)
138
+
139
+
140
+ @projects.command("create")
141
+ @click.argument("name")
142
+ @click.option("--description", help="Project description.")
143
+ @click.pass_context
144
+ def create_project(ctx, name, description):
145
+ """Create a new project."""
146
+ from langsmith.utils import LangSmithConflictError
147
+
148
+ client = langsmith.Client()
149
+ try:
150
+ project = client.create_project(project_name=name, description=description)
151
+ if ctx.obj.get("json"):
152
+ # Use SDK's Pydantic model directly
153
+ data = project.model_dump(mode="json")
154
+ click.echo(json.dumps(data, default=str))
155
+ return
156
+
157
+ console.print(
158
+ f"[green]Created project {project.name}[/green] (ID: {project.id})"
159
+ )
160
+ except LangSmithConflictError:
161
+ # Project already exists - handle gracefully for idempotency
162
+ console.print(f"[yellow]Project {name} already exists.[/yellow]")
@@ -0,0 +1,125 @@
1
+ import click
2
+ from rich.console import Console
3
+ from rich.table import Table
4
+ import langsmith
5
+ import json
6
+ from langsmith_cli.utils import (
7
+ print_empty_result_message,
8
+ parse_comma_separated_list,
9
+ )
10
+
11
+ console = Console()
12
+
13
+
14
+ @click.group()
15
+ def prompts():
16
+ """Manage LangSmith prompts."""
17
+ pass
18
+
19
+
20
+ @prompts.command("list")
21
+ @click.option("--limit", default=20, help="Limit number of prompts (default 20).")
22
+ @click.option(
23
+ "--is-public", type=bool, default=None, help="Filter by public/private status."
24
+ )
25
+ @click.pass_context
26
+ def list_prompts(ctx, limit, is_public):
27
+ """List available prompt repositories."""
28
+ client = langsmith.Client()
29
+ # list_prompts returns ListPromptsResponse with .repos attribute
30
+ result = client.list_prompts(limit=limit, is_public=is_public)
31
+ prompts_list = result.repos
32
+
33
+ if ctx.obj.get("json"):
34
+ # Use SDK's Pydantic models with focused field selection for context efficiency
35
+ data = [
36
+ p.model_dump(
37
+ include={
38
+ "repo_handle",
39
+ "description",
40
+ "id",
41
+ "is_public",
42
+ "tags",
43
+ "owner",
44
+ "full_name",
45
+ "num_likes",
46
+ "num_downloads",
47
+ "num_views",
48
+ "created_at",
49
+ "updated_at",
50
+ },
51
+ mode="json",
52
+ )
53
+ for p in prompts_list
54
+ ]
55
+ click.echo(json.dumps(data, default=str))
56
+ return
57
+
58
+ table = Table(title="Prompts")
59
+ table.add_column("Repo", style="cyan")
60
+ table.add_column("Description")
61
+ table.add_column("Owner", style="dim")
62
+
63
+ for p in prompts_list:
64
+ table.add_row(p.full_name, p.description or "", p.owner)
65
+
66
+ if not prompts_list:
67
+ print_empty_result_message(console, "prompts")
68
+ else:
69
+ console.print(table)
70
+
71
+
72
+ @prompts.command("get")
73
+ @click.argument("name")
74
+ @click.option("--commit", help="Commit hash or tag.")
75
+ @click.pass_context
76
+ def get_prompt(ctx, name, commit):
77
+ """Fetch a prompt template."""
78
+ client = langsmith.Client()
79
+ # pull_prompt returns the prompt object (might be LangChain PromptTemplate)
80
+ prompt_obj = client.pull_prompt(name + (f":{commit}" if commit else ""))
81
+
82
+ # We want a context-efficient representation, usually the template string
83
+ # Try to convert to dict or extract template
84
+ if hasattr(prompt_obj, "to_json"):
85
+ data = prompt_obj.to_json()
86
+ else:
87
+ # Fallback to string representation if it's not JSON serializable trivially
88
+ data = {"prompt": str(prompt_obj)}
89
+
90
+ if ctx.obj.get("json"):
91
+ click.echo(json.dumps(data, default=str))
92
+ return
93
+
94
+ console.print(f"[bold]Prompt:[/bold] {name}")
95
+ console.print("-" * 20)
96
+ console.print(str(prompt_obj))
97
+
98
+
99
+ @prompts.command("push")
100
+ @click.argument("name")
101
+ @click.argument("file_path", type=click.Path(exists=True))
102
+ @click.option("--description", help="Prompt description.")
103
+ @click.option("--tags", help="Comma-separated tags.")
104
+ @click.option("--is-public", type=bool, default=False, help="Make prompt public.")
105
+ @click.pass_context
106
+ def push_prompt(ctx, name, file_path, description, tags, is_public):
107
+ """Push a local prompt file to LangSmith."""
108
+ client = langsmith.Client()
109
+
110
+ with open(file_path, "r") as f:
111
+ content = f.read()
112
+
113
+ # Parse tags if provided
114
+ tags_list = parse_comma_separated_list(tags)
115
+
116
+ # Push prompt with metadata
117
+ client.push_prompt(
118
+ prompt_identifier=name,
119
+ object=content,
120
+ description=description,
121
+ tags=tags_list,
122
+ is_public=is_public,
123
+ )
124
+
125
+ console.print(f"[green]Successfully pushed prompt to {name}[/green]")