doushi-cli 0.1.0__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.
- doushi/__init__.py +4 -0
- doushi/client.py +255 -0
- doushi/commands/__init__.py +1 -0
- doushi/commands/auth.py +166 -0
- doushi/commands/demo.py +79 -0
- doushi/commands/export.py +76 -0
- doushi/commands/logs.py +85 -0
- doushi/commands/predict.py +226 -0
- doushi/commands/projects.py +125 -0
- doushi/commands/train.py +198 -0
- doushi/config.py +111 -0
- doushi/main.py +79 -0
- doushi/templates.py +109 -0
- doushi/ui.py +145 -0
- doushi_cli-0.1.0.dist-info/METADATA +236 -0
- doushi_cli-0.1.0.dist-info/RECORD +20 -0
- doushi_cli-0.1.0.dist-info/WHEEL +5 -0
- doushi_cli-0.1.0.dist-info/entry_points.txt +3 -0
- doushi_cli-0.1.0.dist-info/licenses/LICENSE +21 -0
- doushi_cli-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
"""Prediction and inference command for Doushi CLI (predict)."""
|
|
2
|
+
|
|
3
|
+
import csv
|
|
4
|
+
import json
|
|
5
|
+
import sys
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any, Dict, List, Optional
|
|
8
|
+
import typer
|
|
9
|
+
from rich.table import Table
|
|
10
|
+
|
|
11
|
+
from doushi.client import DoushiClient, DoushiAPIError
|
|
12
|
+
from doushi.ui import (
|
|
13
|
+
console,
|
|
14
|
+
err_console,
|
|
15
|
+
print_error_panel,
|
|
16
|
+
print_json,
|
|
17
|
+
print_success,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
predict_app = typer.Typer(help="Run live inference on trained Doushi AI models.")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def load_batch_file(file_path: Path) -> List[Dict[str, Any]]:
|
|
24
|
+
"""Load sample records from various file formats (.json, .parquet, .xls, .xlsx, .csv, .tsv)."""
|
|
25
|
+
ext = file_path.suffix.lower()
|
|
26
|
+
|
|
27
|
+
# 1. JSON
|
|
28
|
+
if ext == ".json":
|
|
29
|
+
with open(file_path, "r", encoding="utf-8") as f:
|
|
30
|
+
data = json.load(f)
|
|
31
|
+
if isinstance(data, list):
|
|
32
|
+
return data
|
|
33
|
+
elif isinstance(data, dict):
|
|
34
|
+
return [data]
|
|
35
|
+
raise ValueError("JSON file must contain an array of sample objects or a single sample object.")
|
|
36
|
+
|
|
37
|
+
# 2. Parquet
|
|
38
|
+
elif ext == ".parquet":
|
|
39
|
+
try:
|
|
40
|
+
import pandas as pd
|
|
41
|
+
df = pd.read_parquet(file_path)
|
|
42
|
+
return df.to_dict(orient="records")
|
|
43
|
+
except ImportError:
|
|
44
|
+
try:
|
|
45
|
+
import pyarrow.parquet as pq
|
|
46
|
+
table = pq.read_table(file_path)
|
|
47
|
+
return table.to_pylist()
|
|
48
|
+
except ImportError:
|
|
49
|
+
raise ImportError(
|
|
50
|
+
"To read .parquet files, please install pandas or pyarrow: pip install pandas pyarrow"
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
# 3. Excel (.xls, .xlsx)
|
|
54
|
+
elif ext in [".xls", ".xlsx"]:
|
|
55
|
+
try:
|
|
56
|
+
import pandas as pd
|
|
57
|
+
df = pd.read_excel(file_path)
|
|
58
|
+
return df.to_dict(orient="records")
|
|
59
|
+
except ImportError:
|
|
60
|
+
raise ImportError(
|
|
61
|
+
"To read Excel files (.xls, .xlsx), please install pandas and openpyxl: pip install pandas openpyxl"
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
# 4. Delimited Text (CSV, TSV, TXT)
|
|
65
|
+
else:
|
|
66
|
+
delimiter = "\t" if ext == ".tsv" else ","
|
|
67
|
+
samples: List[Dict[str, Any]] = []
|
|
68
|
+
with open(file_path, "r", encoding="utf-8", errors="replace") as f:
|
|
69
|
+
sample_header = f.read(2048)
|
|
70
|
+
f.seek(0)
|
|
71
|
+
if ext != ".tsv" and ";" in sample_header and "," not in sample_header:
|
|
72
|
+
delimiter = ";"
|
|
73
|
+
|
|
74
|
+
reader = csv.DictReader(f, delimiter=delimiter)
|
|
75
|
+
for row in reader:
|
|
76
|
+
parsed_row = {}
|
|
77
|
+
for k, v in row.items():
|
|
78
|
+
if v is None or v == "":
|
|
79
|
+
parsed_row[k] = None
|
|
80
|
+
else:
|
|
81
|
+
try:
|
|
82
|
+
parsed_row[k] = float(v) if "." in v else int(v)
|
|
83
|
+
except ValueError:
|
|
84
|
+
parsed_row[k] = v
|
|
85
|
+
samples.append(parsed_row)
|
|
86
|
+
return samples
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
@predict_app.command(name="predict")
|
|
90
|
+
def predict(
|
|
91
|
+
project_id: str = typer.Argument(..., help="Unique ID of the trained project"),
|
|
92
|
+
data: Optional[str] = typer.Option(
|
|
93
|
+
None,
|
|
94
|
+
"--data",
|
|
95
|
+
"-d",
|
|
96
|
+
help="JSON string of feature inputs (e.g. '{\"age\": 30, \"tenure\": 12}')",
|
|
97
|
+
),
|
|
98
|
+
file_path: Optional[Path] = typer.Option(
|
|
99
|
+
None,
|
|
100
|
+
"--file",
|
|
101
|
+
"-f",
|
|
102
|
+
help="Path to CSV, JSON, Parquet, or Excel file containing batch samples for inference",
|
|
103
|
+
exists=True,
|
|
104
|
+
file_okay=True,
|
|
105
|
+
dir_okay=False,
|
|
106
|
+
),
|
|
107
|
+
output: Optional[Path] = typer.Option(
|
|
108
|
+
None,
|
|
109
|
+
"--output",
|
|
110
|
+
"-o",
|
|
111
|
+
help="Path to save prediction outputs (.csv or .json)",
|
|
112
|
+
),
|
|
113
|
+
json_output: bool = typer.Option(
|
|
114
|
+
False,
|
|
115
|
+
"--json",
|
|
116
|
+
help="Output predictions as raw JSON",
|
|
117
|
+
),
|
|
118
|
+
) -> None:
|
|
119
|
+
"""Run live prediction using inline JSON, a CSV/JSON/Parquet/Excel file, or piped stdin."""
|
|
120
|
+
client = DoushiClient()
|
|
121
|
+
client.check_auth_or_exit()
|
|
122
|
+
|
|
123
|
+
payload_data = None
|
|
124
|
+
|
|
125
|
+
# 1. Check direct CLI JSON argument
|
|
126
|
+
if data:
|
|
127
|
+
try:
|
|
128
|
+
payload_data = json.loads(data)
|
|
129
|
+
except json.JSONDecodeError:
|
|
130
|
+
print_error_panel(
|
|
131
|
+
"Invalid JSON",
|
|
132
|
+
f"Could not parse --data as valid JSON: {data}",
|
|
133
|
+
remedy="Ensure proper quoting: --data '{\"feature_1\": 10, \"feature_2\": 20}'"
|
|
134
|
+
)
|
|
135
|
+
raise typer.Exit(code=1)
|
|
136
|
+
|
|
137
|
+
# 2. Check file input
|
|
138
|
+
elif file_path:
|
|
139
|
+
try:
|
|
140
|
+
payload_data = load_batch_file(file_path)
|
|
141
|
+
except Exception as e:
|
|
142
|
+
print_error_panel("File Loading Error", str(e))
|
|
143
|
+
raise typer.Exit(code=1)
|
|
144
|
+
|
|
145
|
+
# 3. Check stdin pipe
|
|
146
|
+
elif not sys.stdin.isatty():
|
|
147
|
+
stdin_content = sys.stdin.read().strip()
|
|
148
|
+
if stdin_content:
|
|
149
|
+
try:
|
|
150
|
+
payload_data = json.loads(stdin_content)
|
|
151
|
+
except json.JSONDecodeError:
|
|
152
|
+
# Try parsing as CSV
|
|
153
|
+
import io
|
|
154
|
+
reader = csv.DictReader(io.StringIO(stdin_content))
|
|
155
|
+
samples = [row for row in reader]
|
|
156
|
+
if samples:
|
|
157
|
+
payload_data = samples
|
|
158
|
+
else:
|
|
159
|
+
print_error_panel("Invalid Stdin Input", "Could not parse standard input as JSON or CSV.")
|
|
160
|
+
raise typer.Exit(code=1)
|
|
161
|
+
|
|
162
|
+
if payload_data is None:
|
|
163
|
+
print_error_panel(
|
|
164
|
+
"Missing Input Data",
|
|
165
|
+
"No prediction data provided.",
|
|
166
|
+
remedy="Provide data using:\n • --data '{\"feature\": 12}'\n • --file test_data.csv\n • cat test_data.csv | doushi predict <id>"
|
|
167
|
+
)
|
|
168
|
+
raise typer.Exit(code=1)
|
|
169
|
+
|
|
170
|
+
# Execute Prediction
|
|
171
|
+
is_interactive = sys.stdout.isatty() and not json_output
|
|
172
|
+
if is_interactive:
|
|
173
|
+
with console.status(f"[bold cyan]Computing prediction for project {project_id}...[/bold cyan]"):
|
|
174
|
+
try:
|
|
175
|
+
result = client.predict(project_id, payload_data)
|
|
176
|
+
except DoushiAPIError as e:
|
|
177
|
+
print_error_panel("Inference Failed", e.message, status_code=e.status_code)
|
|
178
|
+
raise typer.Exit(code=1)
|
|
179
|
+
else:
|
|
180
|
+
try:
|
|
181
|
+
result = client.predict(project_id, payload_data)
|
|
182
|
+
except DoushiAPIError as e:
|
|
183
|
+
err_console.print(f"Error: {e.message}")
|
|
184
|
+
raise typer.Exit(code=1)
|
|
185
|
+
|
|
186
|
+
# Output handling
|
|
187
|
+
if output:
|
|
188
|
+
if output.suffix.lower() == ".json":
|
|
189
|
+
with open(output, "w", encoding="utf-8") as f:
|
|
190
|
+
json.dump(result, f, indent=2)
|
|
191
|
+
else:
|
|
192
|
+
# Write CSV
|
|
193
|
+
predictions = result.get("prediction") or result.get("predictions")
|
|
194
|
+
if isinstance(predictions, list):
|
|
195
|
+
with open(output, "w", encoding="utf-8", newline="") as f:
|
|
196
|
+
writer = csv.writer(f)
|
|
197
|
+
writer.writerow(["prediction"])
|
|
198
|
+
for p in predictions:
|
|
199
|
+
writer.writerow([p])
|
|
200
|
+
else:
|
|
201
|
+
with open(output, "w", encoding="utf-8") as f:
|
|
202
|
+
f.write(str(predictions))
|
|
203
|
+
print_success(f"Predictions saved to {output}")
|
|
204
|
+
return
|
|
205
|
+
|
|
206
|
+
if json_output or not sys.stdout.isatty():
|
|
207
|
+
print_json(result)
|
|
208
|
+
return
|
|
209
|
+
|
|
210
|
+
# Formatted Rich Output
|
|
211
|
+
pred = result.get("prediction") if "prediction" in result else result.get("predictions")
|
|
212
|
+
latency = result.get("latency_ms", "-")
|
|
213
|
+
|
|
214
|
+
table = Table(title=f"Prediction Result (Latency: {latency}ms)", border_style="green")
|
|
215
|
+
table.add_column("Sample #", style="dim", justify="right")
|
|
216
|
+
table.add_column("Prediction", style="bold green")
|
|
217
|
+
|
|
218
|
+
if isinstance(pred, list):
|
|
219
|
+
for i, val in enumerate(pred[:50]):
|
|
220
|
+
table.add_row(str(i + 1), str(val))
|
|
221
|
+
if len(pred) > 50:
|
|
222
|
+
table.add_row("...", f"[dim]and {len(pred) - 50} more samples[/dim]")
|
|
223
|
+
else:
|
|
224
|
+
table.add_row("1", str(pred))
|
|
225
|
+
|
|
226
|
+
console.print(table)
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
"""Project management commands for Doushi CLI (list, view, delete, init)."""
|
|
2
|
+
|
|
3
|
+
from typing import Optional
|
|
4
|
+
import typer
|
|
5
|
+
from rich.panel import Panel
|
|
6
|
+
from rich.table import Table
|
|
7
|
+
from rich.prompt import Prompt, Confirm
|
|
8
|
+
|
|
9
|
+
from doushi.client import DoushiClient, DoushiAPIError
|
|
10
|
+
from doushi.ui import (
|
|
11
|
+
console,
|
|
12
|
+
err_console,
|
|
13
|
+
print_error_panel,
|
|
14
|
+
print_info,
|
|
15
|
+
print_json,
|
|
16
|
+
print_projects_table,
|
|
17
|
+
print_success,
|
|
18
|
+
print_warning,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
projects_app = typer.Typer(help="Manage and inspect your Doushi AI projects.")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@projects_app.command(name="list")
|
|
25
|
+
@projects_app.command(name="ls")
|
|
26
|
+
def list_projects(
|
|
27
|
+
json_output: bool = typer.Option(False, "--json", help="Output list as JSON"),
|
|
28
|
+
) -> None:
|
|
29
|
+
"""List all AI projects in your organization."""
|
|
30
|
+
client = DoushiClient()
|
|
31
|
+
with console.status("[bold cyan]Fetching projects...[/bold cyan]"):
|
|
32
|
+
try:
|
|
33
|
+
projects = client.list_projects()
|
|
34
|
+
except DoushiAPIError as e:
|
|
35
|
+
print_error_panel("Failed to List Projects", e.message, status_code=e.status_code)
|
|
36
|
+
raise typer.Exit(code=1)
|
|
37
|
+
|
|
38
|
+
if json_output:
|
|
39
|
+
print_json(projects)
|
|
40
|
+
return
|
|
41
|
+
|
|
42
|
+
print_projects_table(projects)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@projects_app.command(name="view")
|
|
46
|
+
def view_project(
|
|
47
|
+
project_id: str = typer.Argument(..., help="Unique ID of the project"),
|
|
48
|
+
json_output: bool = typer.Option(False, "--json", help="Output details as JSON"),
|
|
49
|
+
) -> None:
|
|
50
|
+
"""View detailed metrics, hyperparameters, and status for a project."""
|
|
51
|
+
client = DoushiClient()
|
|
52
|
+
with console.status(f"[bold cyan]Fetching project {project_id}...[/bold cyan]"):
|
|
53
|
+
try:
|
|
54
|
+
p = client.get_project(project_id)
|
|
55
|
+
except DoushiAPIError as e:
|
|
56
|
+
print_error_panel("Project Not Found", e.message, status_code=e.status_code)
|
|
57
|
+
raise typer.Exit(code=1)
|
|
58
|
+
|
|
59
|
+
if json_output:
|
|
60
|
+
print_json(p)
|
|
61
|
+
return
|
|
62
|
+
|
|
63
|
+
status = str(p.get("status", "unknown")).upper()
|
|
64
|
+
name = p.get("name", "Untitled")
|
|
65
|
+
|
|
66
|
+
console.print(f"\n[bold white]Project:[/bold white] [bold cyan]{name}[/bold cyan] ([dim]{project_id}[/dim])")
|
|
67
|
+
console.print(f"[bold white]Status:[/bold white] [bold]{status}[/bold]\n")
|
|
68
|
+
|
|
69
|
+
# Overview table
|
|
70
|
+
table = Table(title="Project Details", border_style="dim")
|
|
71
|
+
table.add_column("Property", style="bold white")
|
|
72
|
+
table.add_column("Value", style="cyan")
|
|
73
|
+
|
|
74
|
+
table.add_row("ID", str(p.get("id", "-")))
|
|
75
|
+
table.add_row("Name", name)
|
|
76
|
+
table.add_row("Status", status)
|
|
77
|
+
table.add_row("Prompt Goal", str(p.get("prompt", "-")))
|
|
78
|
+
table.add_row("Dataset Path", str(p.get("dataset_path", "-")))
|
|
79
|
+
table.add_row("Model Provider", str(p.get("provider", "gemini")))
|
|
80
|
+
table.add_row("Current Iteration", str(p.get("current_iteration", 0)))
|
|
81
|
+
table.add_row("Created At", str(p.get("created_at", "-")))
|
|
82
|
+
|
|
83
|
+
console.print(table)
|
|
84
|
+
|
|
85
|
+
# Metrics section
|
|
86
|
+
metrics = p.get("metrics")
|
|
87
|
+
if metrics and isinstance(metrics, dict):
|
|
88
|
+
m_table = Table(title="Model Evaluation Metrics", border_style="green")
|
|
89
|
+
m_table.add_column("Metric", style="bold white")
|
|
90
|
+
m_table.add_column("Score", style="bold green")
|
|
91
|
+
for k, v in metrics.items():
|
|
92
|
+
if isinstance(v, (int, float)):
|
|
93
|
+
m_table.add_row(k.upper(), f"{v:.4f}")
|
|
94
|
+
else:
|
|
95
|
+
m_table.add_row(k.upper(), str(v))
|
|
96
|
+
console.print(m_table)
|
|
97
|
+
|
|
98
|
+
# Error section
|
|
99
|
+
error = p.get("error")
|
|
100
|
+
if error:
|
|
101
|
+
print_error_panel("Model Failure Error", str(error))
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
@projects_app.command(name="delete")
|
|
105
|
+
def delete_project(
|
|
106
|
+
project_id: str = typer.Argument(..., help="Unique ID of the project to delete"),
|
|
107
|
+
yes: bool = typer.Option(False, "--yes", "-y", help="Skip confirmation prompt"),
|
|
108
|
+
) -> None:
|
|
109
|
+
"""Delete a project and its associated artifacts."""
|
|
110
|
+
client = DoushiClient()
|
|
111
|
+
|
|
112
|
+
if not yes:
|
|
113
|
+
confirm = Confirm.ask(f"[bold yellow]Are you sure you want to permanently delete project '{project_id}'?[/bold yellow]")
|
|
114
|
+
if not confirm:
|
|
115
|
+
print_info("Deletion cancelled.")
|
|
116
|
+
return
|
|
117
|
+
|
|
118
|
+
with console.status(f"[bold red]Deleting project {project_id}...[/bold red]"):
|
|
119
|
+
try:
|
|
120
|
+
client.delete_project(project_id)
|
|
121
|
+
except DoushiAPIError as e:
|
|
122
|
+
print_error_panel("Deletion Failed", e.message, status_code=e.status_code)
|
|
123
|
+
raise typer.Exit(code=1)
|
|
124
|
+
|
|
125
|
+
print_success(f"Project '{project_id}' successfully deleted.")
|
doushi/commands/train.py
ADDED
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
"""Model training and creation command for Doushi CLI (train / create)."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import sys
|
|
5
|
+
import time
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Optional
|
|
8
|
+
import typer
|
|
9
|
+
from rich.panel import Panel
|
|
10
|
+
from rich.table import Table
|
|
11
|
+
from rich.prompt import Prompt
|
|
12
|
+
|
|
13
|
+
from doushi.client import DoushiClient, DoushiAPIError
|
|
14
|
+
from doushi.ui import (
|
|
15
|
+
console,
|
|
16
|
+
err_console,
|
|
17
|
+
print_banner,
|
|
18
|
+
print_error_panel,
|
|
19
|
+
print_info,
|
|
20
|
+
print_json,
|
|
21
|
+
print_success,
|
|
22
|
+
print_warning,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
train_app = typer.Typer(help="Train and build autonomous ML models from datasets.")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@train_app.command(name="train")
|
|
29
|
+
@train_app.command(name="create")
|
|
30
|
+
def train(
|
|
31
|
+
dataset_file: Path = typer.Argument(
|
|
32
|
+
...,
|
|
33
|
+
help="Path to local dataset file (.csv, .parquet, .xlsx, .tsv, .json)",
|
|
34
|
+
exists=True,
|
|
35
|
+
file_okay=True,
|
|
36
|
+
dir_okay=False,
|
|
37
|
+
readable=True,
|
|
38
|
+
),
|
|
39
|
+
goal: Optional[str] = typer.Option(
|
|
40
|
+
None,
|
|
41
|
+
"--goal",
|
|
42
|
+
"-g",
|
|
43
|
+
help="Natural language prediction goal (e.g. 'Predict customer churn probability')",
|
|
44
|
+
),
|
|
45
|
+
name: Optional[str] = typer.Option(
|
|
46
|
+
None,
|
|
47
|
+
"--name",
|
|
48
|
+
"-n",
|
|
49
|
+
help="Project name (defaults to dataset filename)",
|
|
50
|
+
),
|
|
51
|
+
follow: bool = typer.Option(
|
|
52
|
+
True,
|
|
53
|
+
"--follow/--no-follow",
|
|
54
|
+
"-f/-F",
|
|
55
|
+
help="Stream agent progress until model training completes.",
|
|
56
|
+
),
|
|
57
|
+
json_output: bool = typer.Option(
|
|
58
|
+
False,
|
|
59
|
+
"--json",
|
|
60
|
+
help="Output created project details as JSON",
|
|
61
|
+
),
|
|
62
|
+
) -> None:
|
|
63
|
+
"""Upload dataset and train an autonomous ML model with agent self-healing."""
|
|
64
|
+
client = DoushiClient()
|
|
65
|
+
client.check_auth_or_exit()
|
|
66
|
+
|
|
67
|
+
# 1. Fetch user account info for tier limit verification
|
|
68
|
+
with console.status("[bold cyan]Checking account quotas & tier limits...[/bold cyan]"):
|
|
69
|
+
try:
|
|
70
|
+
whoami_data = client.get_whoami()
|
|
71
|
+
org = whoami_data.get("organization") or {}
|
|
72
|
+
tier = str(org.get("tier", "free")).lower()
|
|
73
|
+
except Exception:
|
|
74
|
+
tier = "free"
|
|
75
|
+
|
|
76
|
+
# 2. Local Pre-flight check on dataset size
|
|
77
|
+
client.check_file_limits(dataset_file, tier=tier)
|
|
78
|
+
|
|
79
|
+
# 3. Prompt for goal if missing
|
|
80
|
+
if not goal:
|
|
81
|
+
print_banner()
|
|
82
|
+
console.print(f"\n[bold white]Dataset:[/bold white] [bold cyan]{dataset_file.name}[/bold cyan]")
|
|
83
|
+
console.print(f"[bold white]Size:[/bold white] [dim]{dataset_file.stat().st_size / (1024*1024):.2f} MB[/dim]\n")
|
|
84
|
+
goal = Prompt.ask(
|
|
85
|
+
"[bold green]🎯 What is your prediction goal for this dataset?[/bold green]\n"
|
|
86
|
+
"[dim](e.g. 'Predict customer churn', 'Forecast monthly revenue', 'Classify spam')[/dim]"
|
|
87
|
+
)
|
|
88
|
+
if not goal or not goal.strip():
|
|
89
|
+
print_error_panel("Goal Required", "A prediction goal is required for the autonomous agent to build the model.")
|
|
90
|
+
raise typer.Exit(code=1)
|
|
91
|
+
|
|
92
|
+
project_name = name or dataset_file.stem.replace("_", " ").replace("-", " ").title()
|
|
93
|
+
|
|
94
|
+
# 4. Create Project
|
|
95
|
+
with console.status(f"[bold cyan]Initializing project '{project_name}'...[/bold cyan]"):
|
|
96
|
+
try:
|
|
97
|
+
proj_data = client.create_project(name=project_name)
|
|
98
|
+
project_id = proj_data.get("project_id") or proj_data.get("id")
|
|
99
|
+
except DoushiAPIError as e:
|
|
100
|
+
print_error_panel("Project Creation Failed", e.message, status_code=e.status_code)
|
|
101
|
+
raise typer.Exit(code=1)
|
|
102
|
+
|
|
103
|
+
# 5. Request S3 presigned URL
|
|
104
|
+
file_size = dataset_file.stat().st_size
|
|
105
|
+
with console.status("[bold cyan]Requesting secure storage upload URL...[/bold cyan]"):
|
|
106
|
+
try:
|
|
107
|
+
upload_info = client.request_upload_url(
|
|
108
|
+
project_id=project_id,
|
|
109
|
+
filename=dataset_file.name,
|
|
110
|
+
file_size=file_size,
|
|
111
|
+
)
|
|
112
|
+
presigned_url = upload_info["url"]
|
|
113
|
+
except DoushiAPIError as e:
|
|
114
|
+
print_error_panel("Upload Request Failed", e.message, status_code=e.status_code)
|
|
115
|
+
raise typer.Exit(code=1)
|
|
116
|
+
|
|
117
|
+
# 6. Stream upload file to S3
|
|
118
|
+
client.upload_file_to_s3(presigned_url, dataset_file)
|
|
119
|
+
print_success(f"Dataset '{dataset_file.name}' uploaded successfully.")
|
|
120
|
+
|
|
121
|
+
# 7. Start Agent Pipeline
|
|
122
|
+
with console.status("[bold cyan]Dispatching autonomous agent pipeline...[/bold cyan]"):
|
|
123
|
+
try:
|
|
124
|
+
client.start_pipeline(project_id=project_id, prompt=goal)
|
|
125
|
+
except DoushiAPIError as e:
|
|
126
|
+
print_error_panel("Agent Dispatch Failed", e.message, status_code=e.status_code)
|
|
127
|
+
raise typer.Exit(code=1)
|
|
128
|
+
|
|
129
|
+
print_success(f"Autonomous agent started for project [bold cyan]{project_id}[/bold cyan].")
|
|
130
|
+
|
|
131
|
+
if json_output:
|
|
132
|
+
print_json({
|
|
133
|
+
"project_id": project_id,
|
|
134
|
+
"name": project_name,
|
|
135
|
+
"goal": goal,
|
|
136
|
+
"dataset": dataset_file.name,
|
|
137
|
+
"status": "queued"
|
|
138
|
+
})
|
|
139
|
+
return
|
|
140
|
+
|
|
141
|
+
if not follow:
|
|
142
|
+
console.print(f"\n👉 Track status: [bold cyan]doushi view {project_id}[/bold cyan]")
|
|
143
|
+
console.print(f"👉 Stream logs: [bold cyan]doushi logs {project_id} -f[/bold cyan]\n")
|
|
144
|
+
return
|
|
145
|
+
|
|
146
|
+
# 8. Follow / Stream agent progress
|
|
147
|
+
console.print("\n[bold white]Watching Autonomous Agent Execution[/bold white] [dim](Press Ctrl+C to detach)[/dim]\n")
|
|
148
|
+
|
|
149
|
+
last_status = None
|
|
150
|
+
with console.status("[bold yellow]Agent analyzing dataset schema & selecting ML strategy...[/bold yellow]") as status_spinner:
|
|
151
|
+
for _ in range(120): # Wait up to 10 minutes
|
|
152
|
+
time.sleep(5)
|
|
153
|
+
try:
|
|
154
|
+
p = client.get_project(project_id)
|
|
155
|
+
except Exception:
|
|
156
|
+
continue
|
|
157
|
+
|
|
158
|
+
current_status = str(p.get("status", "pending")).lower()
|
|
159
|
+
|
|
160
|
+
if current_status != last_status:
|
|
161
|
+
last_status = current_status
|
|
162
|
+
if current_status in ["queued", "pending"]:
|
|
163
|
+
status_spinner.update("[bold blue]Job queued in compute worker...[/bold blue]")
|
|
164
|
+
elif current_status in ["running", "training"]:
|
|
165
|
+
status_spinner.update("[bold yellow]Agent executing sandbox code & optimizing hyperparameters...[/bold yellow]")
|
|
166
|
+
elif current_status == "success":
|
|
167
|
+
break
|
|
168
|
+
elif current_status == "failed":
|
|
169
|
+
break
|
|
170
|
+
|
|
171
|
+
if current_status == "success":
|
|
172
|
+
console.print("\n")
|
|
173
|
+
print_success(f"Model successfully trained and ready for inference!")
|
|
174
|
+
|
|
175
|
+
metrics = p.get("metrics") or {}
|
|
176
|
+
if isinstance(metrics, dict) and metrics:
|
|
177
|
+
m_table = Table(title="Model Evaluation Performance", border_style="green")
|
|
178
|
+
m_table.add_column("Metric", style="bold white")
|
|
179
|
+
m_table.add_column("Score", style="bold green")
|
|
180
|
+
for k, v in metrics.items():
|
|
181
|
+
val_str = f"{v:.4f}" if isinstance(v, (int, float)) else str(v)
|
|
182
|
+
m_table.add_row(k.upper(), val_str)
|
|
183
|
+
console.print(m_table)
|
|
184
|
+
|
|
185
|
+
console.print("\n[bold green]✦ Next Steps:[/bold green]")
|
|
186
|
+
console.print(f" • Run live prediction: [bold cyan]doushi predict {project_id} --data '{{...}}'[/bold cyan]")
|
|
187
|
+
console.print(f" • Export model & code: [bold cyan]doushi export {project_id} --out ./model-bundle/[/bold cyan]")
|
|
188
|
+
console.print(f" • Inspect project: [bold cyan]doushi view {project_id}[/bold cyan]\n")
|
|
189
|
+
|
|
190
|
+
elif current_status == "failed":
|
|
191
|
+
console.print("\n")
|
|
192
|
+
err_msg = p.get("error") or "Unknown error occurred during agent training."
|
|
193
|
+
print_error_panel(
|
|
194
|
+
title="Model Training Failed",
|
|
195
|
+
message=str(err_msg),
|
|
196
|
+
remedy=f"View full agent traceback with: doushi logs {project_id}"
|
|
197
|
+
)
|
|
198
|
+
raise typer.Exit(code=1)
|
doushi/config.py
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"""Configuration and credential management for Doushi CLI."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import stat
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any, Dict, Optional
|
|
8
|
+
|
|
9
|
+
DEFAULT_API_URL = "https://api.doushi.ai"
|
|
10
|
+
DEFAULT_DASHBOARD_URL = "https://doushi.ai"
|
|
11
|
+
|
|
12
|
+
DOUSHI_DIR = Path.home() / ".doushi"
|
|
13
|
+
CREDENTIALS_FILE = DOUSHI_DIR / "credentials"
|
|
14
|
+
CONFIG_FILE = DOUSHI_DIR / "config.json"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def ensure_doushi_dir() -> None:
|
|
18
|
+
"""Ensure ~/.doushi directory exists with secure permissions."""
|
|
19
|
+
DOUSHI_DIR.mkdir(parents=True, exist_ok=True)
|
|
20
|
+
try:
|
|
21
|
+
os.chmod(DOUSHI_DIR, stat.S_IRWXU) # 0700
|
|
22
|
+
except Exception:
|
|
23
|
+
pass
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def get_api_url() -> str:
|
|
27
|
+
"""Get active API endpoint from env, config file, or default."""
|
|
28
|
+
if os.getenv("DOUSHI_API_URL"):
|
|
29
|
+
return os.environ["DOUSHI_API_URL"].rstrip("/")
|
|
30
|
+
config = load_config()
|
|
31
|
+
return config.get("api_url", DEFAULT_API_URL).rstrip("/")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def get_api_key() -> Optional[str]:
|
|
35
|
+
"""Get active API key from env or credentials file."""
|
|
36
|
+
# 1. Environment variable override
|
|
37
|
+
if os.getenv("DOUSHI_API_KEY"):
|
|
38
|
+
return os.environ["DOUSHI_API_KEY"].strip()
|
|
39
|
+
|
|
40
|
+
# 2. Stored credentials
|
|
41
|
+
creds = load_credentials()
|
|
42
|
+
current_context = creds.get("current_context", "default")
|
|
43
|
+
contexts = creds.get("contexts", {})
|
|
44
|
+
context_data = contexts.get(current_context, {})
|
|
45
|
+
return context_data.get("api_key")
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def load_credentials() -> Dict[str, Any]:
|
|
49
|
+
"""Load credentials JSON from ~/.doushi/credentials."""
|
|
50
|
+
if not CREDENTIALS_FILE.exists():
|
|
51
|
+
return {"current_context": "default", "contexts": {}}
|
|
52
|
+
try:
|
|
53
|
+
with open(CREDENTIALS_FILE, "r", encoding="utf-8") as f:
|
|
54
|
+
return json.load(f)
|
|
55
|
+
except Exception:
|
|
56
|
+
return {"current_context": "default", "contexts": {}}
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def save_credentials(api_key: str, user_email: Optional[str] = None, org_name: Optional[str] = None, tier: Optional[str] = None, context: str = "default") -> None:
|
|
60
|
+
"""Save credentials to ~/.doushi/credentials with secure 0600 permissions."""
|
|
61
|
+
ensure_doushi_dir()
|
|
62
|
+
creds = load_credentials()
|
|
63
|
+
if "contexts" not in creds:
|
|
64
|
+
creds["contexts"] = {}
|
|
65
|
+
|
|
66
|
+
creds["current_context"] = context
|
|
67
|
+
creds["contexts"][context] = {
|
|
68
|
+
"api_key": api_key,
|
|
69
|
+
"user_email": user_email,
|
|
70
|
+
"org_name": org_name,
|
|
71
|
+
"tier": tier,
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
with open(CREDENTIALS_FILE, "w", encoding="utf-8") as f:
|
|
75
|
+
json.dump(creds, f, indent=2)
|
|
76
|
+
|
|
77
|
+
try:
|
|
78
|
+
os.chmod(CREDENTIALS_FILE, stat.S_IRUSR | stat.S_IWUSR) # 0600
|
|
79
|
+
except Exception:
|
|
80
|
+
pass
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def delete_credentials() -> None:
|
|
84
|
+
"""Remove stored credentials on logout."""
|
|
85
|
+
if CREDENTIALS_FILE.exists():
|
|
86
|
+
try:
|
|
87
|
+
CREDENTIALS_FILE.unlink()
|
|
88
|
+
except Exception:
|
|
89
|
+
pass
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def load_config() -> Dict[str, Any]:
|
|
93
|
+
"""Load settings from ~/.doushi/config.json."""
|
|
94
|
+
if not CONFIG_FILE.exists():
|
|
95
|
+
return {
|
|
96
|
+
"api_url": DEFAULT_API_URL,
|
|
97
|
+
"output_format": "table",
|
|
98
|
+
"auto_open_browser": True
|
|
99
|
+
}
|
|
100
|
+
try:
|
|
101
|
+
with open(CONFIG_FILE, "r", encoding="utf-8") as f:
|
|
102
|
+
return json.load(f)
|
|
103
|
+
except Exception:
|
|
104
|
+
return {"api_url": DEFAULT_API_URL, "output_format": "table"}
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def save_config(config: Dict[str, Any]) -> None:
|
|
108
|
+
"""Save settings to ~/.doushi/config.json."""
|
|
109
|
+
ensure_doushi_dir()
|
|
110
|
+
with open(CONFIG_FILE, "w", encoding="utf-8") as f:
|
|
111
|
+
json.dump(config, f, indent=2)
|