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.
- langsmith_cli/__init__.py +0 -0
- langsmith_cli/commands/auth.py +29 -0
- langsmith_cli/commands/datasets.py +170 -0
- langsmith_cli/commands/examples.py +170 -0
- langsmith_cli/commands/projects.py +162 -0
- langsmith_cli/commands/prompts.py +125 -0
- langsmith_cli/commands/runs.py +545 -0
- langsmith_cli/main.py +45 -0
- langsmith_cli/utils.py +237 -0
- langsmith_cli-0.1.3.dist-info/METADATA +492 -0
- langsmith_cli-0.1.3.dist-info/RECORD +14 -0
- langsmith_cli-0.1.3.dist-info/WHEEL +4 -0
- langsmith_cli-0.1.3.dist-info/entry_points.txt +2 -0
- langsmith_cli-0.1.3.dist-info/licenses/LICENSE +21 -0
|
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]")
|