synth-ai 0.2.2.dev0__py3-none-any.whl → 0.2.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.
- synth_ai/cli/__init__.py +66 -0
- synth_ai/cli/balance.py +205 -0
- synth_ai/cli/calc.py +70 -0
- synth_ai/cli/demo.py +74 -0
- synth_ai/{cli.py → cli/legacy_root_backup.py} +60 -15
- synth_ai/cli/man.py +103 -0
- synth_ai/cli/recent.py +126 -0
- synth_ai/cli/root.py +184 -0
- synth_ai/cli/status.py +126 -0
- synth_ai/cli/traces.py +136 -0
- synth_ai/cli/watch.py +508 -0
- synth_ai/config/base_url.py +53 -0
- synth_ai/environments/examples/crafter_classic/agent_demos/analyze_semantic_words_markdown.py +252 -0
- synth_ai/environments/examples/crafter_classic/agent_demos/crafter_modal_ft/filter_traces_sft_duckdb_v2_backup.py +413 -0
- synth_ai/environments/examples/crafter_classic/agent_demos/crafter_modal_ft/filter_traces_sft_turso.py +646 -0
- synth_ai/environments/examples/crafter_classic/agent_demos/crafter_modal_ft/kick_off_ft_synth.py +34 -0
- synth_ai/environments/examples/crafter_classic/agent_demos/crafter_modal_ft/test_crafter_react_agent_lm_synth.py +1740 -0
- synth_ai/environments/examples/crafter_classic/agent_demos/crafter_modal_ft/test_crafter_react_agent_lm_synth_v2_backup.py +1318 -0
- synth_ai/environments/examples/crafter_classic/agent_demos/crafter_openai_ft/filter_traces_sft_duckdb_v2_backup.py +386 -0
- synth_ai/environments/examples/crafter_classic/agent_demos/crafter_openai_ft/filter_traces_sft_turso.py +580 -0
- synth_ai/environments/examples/crafter_classic/agent_demos/crafter_openai_ft/run_rollouts_for_models_and_compare_v2_backup.py +1352 -0
- synth_ai/environments/examples/crafter_classic/agent_demos/crafter_openai_ft/test_crafter_react_agent_openai_v2_backup.py +2551 -0
- synth_ai/environments/examples/crafter_classic/agent_demos/crafter_trace_evaluation.py +1 -1
- synth_ai/environments/examples/crafter_classic/agent_demos/old/traces/session_crafter_episode_16_15227b68-2906-416f-acc4-d6a9b4fa5828_20250725_001154.json +1363 -1
- synth_ai/environments/examples/crafter_classic/agent_demos/test_crafter_react_agent.py +3 -3
- synth_ai/environments/examples/enron/dataset/corbt___enron_emails_sample_questions/default/0.0.0/293c9fe8170037e01cc9cf5834e0cd5ef6f1a6bb/dataset_info.json +1 -0
- synth_ai/environments/examples/nethack/helpers/achievements.json +64 -0
- synth_ai/environments/examples/red/units/test_exploration_strategy.py +1 -1
- synth_ai/environments/examples/red/units/test_menu_bug_reproduction.py +5 -5
- synth_ai/environments/examples/red/units/test_movement_debug.py +2 -2
- synth_ai/environments/examples/red/units/test_retry_movement.py +1 -1
- synth_ai/environments/examples/sokoban/engine_helpers/vendored/envs/available_envs.json +122 -0
- synth_ai/environments/examples/sokoban/verified_puzzles.json +54987 -0
- synth_ai/experimental/synth_oss.py +446 -0
- synth_ai/learning/core.py +21 -0
- synth_ai/learning/gateway.py +4 -0
- synth_ai/learning/prompts/mipro.py +0 -0
- synth_ai/lm/__init__.py +3 -0
- synth_ai/lm/core/main.py +4 -0
- synth_ai/lm/core/main_v3.py +68 -13
- synth_ai/lm/core/vendor_clients.py +4 -0
- synth_ai/lm/provider_support/openai.py +11 -2
- synth_ai/lm/vendors/base.py +7 -0
- synth_ai/lm/vendors/openai_standard.py +339 -4
- synth_ai/lm/vendors/openai_standard_responses.py +243 -0
- synth_ai/lm/vendors/synth_client.py +155 -5
- synth_ai/lm/warmup.py +54 -17
- synth_ai/tracing/__init__.py +18 -0
- synth_ai/tracing_v1/__init__.py +29 -14
- synth_ai/tracing_v3/config.py +13 -7
- synth_ai/tracing_v3/db_config.py +6 -6
- synth_ai/tracing_v3/turso/manager.py +8 -8
- synth_ai/tui/__main__.py +13 -0
- synth_ai/tui/dashboard.py +329 -0
- synth_ai/v0/tracing/__init__.py +0 -0
- synth_ai/{tracing → v0/tracing}/base_client.py +3 -3
- synth_ai/{tracing → v0/tracing}/client_manager.py +1 -1
- synth_ai/{tracing → v0/tracing}/context.py +1 -1
- synth_ai/{tracing → v0/tracing}/decorators.py +11 -11
- synth_ai/v0/tracing/events/__init__.py +0 -0
- synth_ai/{tracing → v0/tracing}/events/manage.py +4 -4
- synth_ai/{tracing → v0/tracing}/events/scope.py +6 -6
- synth_ai/{tracing → v0/tracing}/events/store.py +3 -3
- synth_ai/{tracing → v0/tracing}/immediate_client.py +6 -6
- synth_ai/{tracing → v0/tracing}/log_client_base.py +2 -2
- synth_ai/{tracing → v0/tracing}/retry_queue.py +3 -3
- synth_ai/{tracing → v0/tracing}/trackers.py +2 -2
- synth_ai/{tracing → v0/tracing}/upload.py +4 -4
- synth_ai/v0/tracing_v1/__init__.py +16 -0
- synth_ai/{tracing_v1 → v0/tracing_v1}/base_client.py +3 -3
- synth_ai/{tracing_v1 → v0/tracing_v1}/client_manager.py +1 -1
- synth_ai/{tracing_v1 → v0/tracing_v1}/context.py +1 -1
- synth_ai/{tracing_v1 → v0/tracing_v1}/decorators.py +11 -11
- synth_ai/v0/tracing_v1/events/__init__.py +0 -0
- synth_ai/{tracing_v1 → v0/tracing_v1}/events/manage.py +4 -4
- synth_ai/{tracing_v1 → v0/tracing_v1}/events/scope.py +6 -6
- synth_ai/{tracing_v1 → v0/tracing_v1}/events/store.py +3 -3
- synth_ai/{tracing_v1 → v0/tracing_v1}/immediate_client.py +6 -6
- synth_ai/{tracing_v1 → v0/tracing_v1}/log_client_base.py +2 -2
- synth_ai/{tracing_v1 → v0/tracing_v1}/retry_queue.py +3 -3
- synth_ai/{tracing_v1 → v0/tracing_v1}/trackers.py +2 -2
- synth_ai/{tracing_v1 → v0/tracing_v1}/upload.py +4 -4
- {synth_ai-0.2.2.dev0.dist-info → synth_ai-0.2.3.dist-info}/METADATA +98 -4
- {synth_ai-0.2.2.dev0.dist-info → synth_ai-0.2.3.dist-info}/RECORD +98 -62
- /synth_ai/{tracing/events/__init__.py → environments/examples/crafter_classic/debug_translation.py} +0 -0
- /synth_ai/{tracing_v1/events/__init__.py → learning/prompts/gepa.py} +0 -0
- /synth_ai/{tracing → v0/tracing}/abstractions.py +0 -0
- /synth_ai/{tracing → v0/tracing}/config.py +0 -0
- /synth_ai/{tracing → v0/tracing}/local.py +0 -0
- /synth_ai/{tracing → v0/tracing}/utils.py +0 -0
- /synth_ai/{tracing_v1 → v0/tracing_v1}/abstractions.py +0 -0
- /synth_ai/{tracing_v1 → v0/tracing_v1}/config.py +0 -0
- /synth_ai/{tracing_v1 → v0/tracing_v1}/local.py +0 -0
- /synth_ai/{tracing_v1 → v0/tracing_v1}/utils.py +0 -0
- {synth_ai-0.2.2.dev0.dist-info → synth_ai-0.2.3.dist-info}/WHEEL +0 -0
- {synth_ai-0.2.2.dev0.dist-info → synth_ai-0.2.3.dist-info}/entry_points.txt +0 -0
- {synth_ai-0.2.2.dev0.dist-info → synth_ai-0.2.3.dist-info}/licenses/LICENSE +0 -0
- {synth_ai-0.2.2.dev0.dist-info → synth_ai-0.2.3.dist-info}/top_level.txt +0 -0
synth_ai/cli/watch.py
ADDED
@@ -0,0 +1,508 @@
|
|
1
|
+
#!/usr/bin/env python3
|
2
|
+
"""
|
3
|
+
CLI visualizer: live watch + experiment listings.
|
4
|
+
|
5
|
+
Placed beside scope.txt for discoverability.
|
6
|
+
"""
|
7
|
+
|
8
|
+
import asyncio
|
9
|
+
from datetime import datetime
|
10
|
+
from typing import Any, Dict, List, Optional, Tuple
|
11
|
+
|
12
|
+
import click
|
13
|
+
from rich.console import Console, Group
|
14
|
+
from rich.table import Table
|
15
|
+
from rich.panel import Panel
|
16
|
+
from rich.align import Align
|
17
|
+
from rich.live import Live
|
18
|
+
from rich import box
|
19
|
+
import sys
|
20
|
+
|
21
|
+
|
22
|
+
class _State:
|
23
|
+
def __init__(self):
|
24
|
+
self.view: str = "experiments" # experiments | experiment | usage | traces | recent
|
25
|
+
self.view_arg: Optional[str] = None
|
26
|
+
self.limit: int = 20
|
27
|
+
self.hours: float = 24.0
|
28
|
+
self.last_msg: str = "Type 'help' for commands. 'q' to quit."
|
29
|
+
self.error: Optional[str] = None
|
30
|
+
# UI state for a visible, blinking cursor in the input field
|
31
|
+
self.cursor_on: bool = True
|
32
|
+
|
33
|
+
|
34
|
+
def _short_id(exp_id: str) -> str:
|
35
|
+
return exp_id[:8] if exp_id else ""
|
36
|
+
|
37
|
+
|
38
|
+
def _format_currency(value: float) -> str:
|
39
|
+
try:
|
40
|
+
return f"${value:.4f}"
|
41
|
+
except Exception:
|
42
|
+
return "$0.0000"
|
43
|
+
|
44
|
+
|
45
|
+
def _format_int(value: Any) -> str:
|
46
|
+
try:
|
47
|
+
return f"{int(value):,}"
|
48
|
+
except Exception:
|
49
|
+
return "0"
|
50
|
+
|
51
|
+
|
52
|
+
async def _fetch_experiments(db_url: str):
|
53
|
+
from synth_ai.tracing_v3.turso.manager import AsyncSQLTraceManager
|
54
|
+
|
55
|
+
db = AsyncSQLTraceManager(db_url)
|
56
|
+
await db.initialize()
|
57
|
+
try:
|
58
|
+
df = await db.query_traces(
|
59
|
+
"""
|
60
|
+
SELECT
|
61
|
+
e.experiment_id,
|
62
|
+
e.name,
|
63
|
+
e.description,
|
64
|
+
e.created_at,
|
65
|
+
COUNT(DISTINCT st.session_id) as num_sessions,
|
66
|
+
COUNT(DISTINCT ev.id) as num_events,
|
67
|
+
COUNT(DISTINCT m.id) as num_messages,
|
68
|
+
SUM(CASE WHEN ev.event_type = 'cais' THEN ev.cost_usd ELSE 0 END) / 100.0 as total_cost,
|
69
|
+
SUM(CASE WHEN ev.event_type = 'cais' THEN ev.total_tokens ELSE 0 END) as total_tokens
|
70
|
+
FROM experiments e
|
71
|
+
LEFT JOIN session_traces st ON e.experiment_id = st.experiment_id
|
72
|
+
LEFT JOIN events ev ON st.session_id = ev.session_id
|
73
|
+
LEFT JOIN messages m ON st.session_id = m.session_id
|
74
|
+
GROUP BY e.experiment_id, e.name, e.description, e.created_at
|
75
|
+
ORDER BY e.created_at DESC
|
76
|
+
"""
|
77
|
+
)
|
78
|
+
return df
|
79
|
+
finally:
|
80
|
+
await db.close()
|
81
|
+
|
82
|
+
|
83
|
+
def _experiments_table(df, limit: Optional[int] = None) -> Table:
|
84
|
+
table = Table(
|
85
|
+
title="Synth AI Experiments",
|
86
|
+
title_style="bold cyan",
|
87
|
+
show_edge=False,
|
88
|
+
box=box.SIMPLE,
|
89
|
+
header_style="bold",
|
90
|
+
pad_edge=False,
|
91
|
+
)
|
92
|
+
for col in ["ID", "Name", "Sessions", "Events", "Msgs", "Cost", "Tokens", "Created"]:
|
93
|
+
table.add_column(col, justify="right" if col in {"Sessions","Events","Msgs","Tokens"} else "left")
|
94
|
+
|
95
|
+
if df is not None and not df.empty:
|
96
|
+
rows = df.itertuples(index=False)
|
97
|
+
count = 0
|
98
|
+
for row in rows:
|
99
|
+
if limit is not None and count >= limit:
|
100
|
+
break
|
101
|
+
count += 1
|
102
|
+
table.add_row(
|
103
|
+
_short_id(getattr(row, "experiment_id", "")),
|
104
|
+
str(getattr(row, "name", "Unnamed"))[:28],
|
105
|
+
_format_int(getattr(row, "num_sessions", 0)),
|
106
|
+
_format_int(getattr(row, "num_events", 0)),
|
107
|
+
_format_int(getattr(row, "num_messages", 0)),
|
108
|
+
_format_currency(float(getattr(row, "total_cost", 0.0) or 0.0)),
|
109
|
+
_format_int(getattr(row, "total_tokens", 0)),
|
110
|
+
str(getattr(row, "created_at", "")),
|
111
|
+
)
|
112
|
+
else:
|
113
|
+
table.add_row("-", "No experiments found", "-", "-", "-", "-", "-", "-")
|
114
|
+
|
115
|
+
return table
|
116
|
+
|
117
|
+
|
118
|
+
async def _experiment_detail(db_url: str, experiment_id: str) -> Dict[str, Any]:
|
119
|
+
from synth_ai.tracing_v3.turso.manager import AsyncSQLTraceManager
|
120
|
+
|
121
|
+
db = AsyncSQLTraceManager(db_url)
|
122
|
+
await db.initialize()
|
123
|
+
try:
|
124
|
+
exp_df = await db.query_traces(
|
125
|
+
"""
|
126
|
+
SELECT * FROM experiments WHERE experiment_id LIKE :exp_id
|
127
|
+
""",
|
128
|
+
{"exp_id": f"{experiment_id}%"},
|
129
|
+
)
|
130
|
+
if exp_df.empty:
|
131
|
+
return {"not_found": True}
|
132
|
+
|
133
|
+
exp = exp_df.iloc[0]
|
134
|
+
|
135
|
+
sessions = await db.get_sessions_by_experiment(exp["experiment_id"])
|
136
|
+
stats_df = await db.query_traces(
|
137
|
+
"""
|
138
|
+
SELECT
|
139
|
+
COUNT(DISTINCT ev.id) as total_events,
|
140
|
+
COUNT(DISTINCT m.id) as total_messages,
|
141
|
+
SUM(CASE WHEN ev.event_type = 'cais' THEN ev.cost_usd ELSE 0 END) / 100.0 as total_cost,
|
142
|
+
SUM(CASE WHEN ev.event_type = 'cais' THEN ev.total_tokens ELSE 0 END) as total_tokens
|
143
|
+
FROM session_traces st
|
144
|
+
LEFT JOIN events ev ON st.session_id = ev.session_id
|
145
|
+
LEFT JOIN messages m ON st.session_id = m.session_id
|
146
|
+
WHERE st.experiment_id = :exp_id
|
147
|
+
""",
|
148
|
+
{"exp_id": exp["experiment_id"]},
|
149
|
+
)
|
150
|
+
stats = stats_df.iloc[0] if not stats_df.empty else None
|
151
|
+
|
152
|
+
return {
|
153
|
+
"experiment": exp,
|
154
|
+
"sessions": sessions or [],
|
155
|
+
"stats": stats,
|
156
|
+
}
|
157
|
+
finally:
|
158
|
+
await db.close()
|
159
|
+
|
160
|
+
|
161
|
+
def _render_experiment_panel(detail: Dict[str, Any]) -> Panel:
|
162
|
+
if detail.get("not_found"):
|
163
|
+
return Panel("No experiment found for given ID", title="Experiment", border_style="red")
|
164
|
+
|
165
|
+
exp = detail["experiment"]
|
166
|
+
stats = detail.get("stats")
|
167
|
+
lines: List[str] = []
|
168
|
+
lines.append(f"[bold]🧪 {exp['name']}[/bold] ([dim]{exp['experiment_id']}[/dim])")
|
169
|
+
if exp.get("description"):
|
170
|
+
lines.append(exp["description"])
|
171
|
+
lines.append("")
|
172
|
+
if stats is not None:
|
173
|
+
lines.append(f"[bold]Stats[/bold] Events: {_format_int(stats['total_events'])} "
|
174
|
+
f"Messages: {_format_int(stats['total_messages'])} "
|
175
|
+
f"Cost: {_format_currency(float(stats['total_cost'] or 0.0))} "
|
176
|
+
f"Tokens: {_format_int(stats['total_tokens'])}")
|
177
|
+
lines.append(f"Created: {exp['created_at']}")
|
178
|
+
lines.append("")
|
179
|
+
sessions = detail.get("sessions", [])
|
180
|
+
if sessions:
|
181
|
+
lines.append("[bold]Sessions[/bold]")
|
182
|
+
for s in sessions[:25]:
|
183
|
+
lines.append(
|
184
|
+
f" - {s['session_id']} [dim]{s['created_at']}[/dim] "
|
185
|
+
f"steps={s['num_timesteps']} events={s['num_events']} msgs={s['num_messages']}"
|
186
|
+
)
|
187
|
+
else:
|
188
|
+
lines.append("No sessions found for experiment.")
|
189
|
+
|
190
|
+
body = "\n".join(lines)
|
191
|
+
return Panel(body, title="Experiment", border_style="cyan")
|
192
|
+
|
193
|
+
|
194
|
+
def register(cli):
|
195
|
+
"""Attach commands to the top-level click group."""
|
196
|
+
|
197
|
+
# Note: The former interactive `watch` command has been removed in favor of
|
198
|
+
# one-off commands (e.g., `synth-ai experiments`, `synth-ai recent`).
|
199
|
+
|
200
|
+
@cli.command()
|
201
|
+
@click.option(
|
202
|
+
"--url",
|
203
|
+
"db_url",
|
204
|
+
default="sqlite+aiosqlite:///./synth_ai.db/dbs/default/data",
|
205
|
+
help="Database URL",
|
206
|
+
)
|
207
|
+
@click.option("--limit", default=50, type=int, help="Max rows to display")
|
208
|
+
def experiments(db_url: str, limit: int):
|
209
|
+
"""Print a snapshot table of experiments."""
|
210
|
+
console = Console()
|
211
|
+
|
212
|
+
async def _run():
|
213
|
+
df = await _fetch_experiments(db_url)
|
214
|
+
table = _experiments_table(df, limit)
|
215
|
+
console.print(table)
|
216
|
+
console.print(
|
217
|
+
"\n[dim]Tip:[/dim] use [cyan]synth-ai experiment <id>[/cyan] for details, "
|
218
|
+
"[cyan]synth-ai usage[/cyan] for model usage.", sep="",
|
219
|
+
)
|
220
|
+
|
221
|
+
asyncio.run(_run())
|
222
|
+
|
223
|
+
@cli.command()
|
224
|
+
@click.argument("experiment_id")
|
225
|
+
@click.option(
|
226
|
+
"--url",
|
227
|
+
"db_url",
|
228
|
+
default="sqlite+aiosqlite:///./synth_ai.db/dbs/default/data",
|
229
|
+
help="Database URL",
|
230
|
+
)
|
231
|
+
def experiment(experiment_id: str, db_url: str):
|
232
|
+
"""Show details and sessions for an experiment (accepts partial ID)."""
|
233
|
+
console = Console()
|
234
|
+
|
235
|
+
async def _run():
|
236
|
+
detail = await _experiment_detail(db_url, experiment_id)
|
237
|
+
panel = _render_experiment_panel(detail)
|
238
|
+
console.print(panel)
|
239
|
+
|
240
|
+
asyncio.run(_run())
|
241
|
+
|
242
|
+
@cli.command()
|
243
|
+
@click.option(
|
244
|
+
"--url",
|
245
|
+
"db_url",
|
246
|
+
default="sqlite+aiosqlite:///./synth_ai.db/dbs/default/data",
|
247
|
+
help="Database URL",
|
248
|
+
)
|
249
|
+
@click.option("--model", "model_name", default=None, help="Filter by model name")
|
250
|
+
def usage(db_url: str, model_name: Optional[str]):
|
251
|
+
"""Show model usage statistics (tokens, cost)."""
|
252
|
+
console = Console()
|
253
|
+
|
254
|
+
async def _run():
|
255
|
+
from synth_ai.tracing_v3.turso.manager import AsyncSQLTraceManager
|
256
|
+
db = AsyncSQLTraceManager(db_url)
|
257
|
+
await db.initialize()
|
258
|
+
try:
|
259
|
+
df = await db.get_model_usage(model_name=model_name)
|
260
|
+
finally:
|
261
|
+
await db.close()
|
262
|
+
|
263
|
+
if df is None or df.empty:
|
264
|
+
console.print("[dim]No model usage data found.[/dim]")
|
265
|
+
return
|
266
|
+
|
267
|
+
table = Table(title="Model Usage", header_style="bold", box=box.SIMPLE)
|
268
|
+
for col in df.columns:
|
269
|
+
table.add_column(str(col))
|
270
|
+
for row in df.itertuples(index=False):
|
271
|
+
table.add_row(*[str(cell) for cell in row])
|
272
|
+
console.print(table)
|
273
|
+
|
274
|
+
asyncio.run(_run())
|
275
|
+
|
276
|
+
|
277
|
+
async def _build_view(console: Console, db_url: str, state: _State):
|
278
|
+
header = f"[bold]Synth AI[/bold] • DB: [dim]{db_url}[/dim] • {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}"
|
279
|
+
help_bar = (
|
280
|
+
"[dim]Commands:[/dim] "
|
281
|
+
"[cyan]experiments[/cyan], [cyan]experiment <id>[/cyan], [cyan]usage [model][/cyan], "
|
282
|
+
"[cyan]traces[/cyan], [cyan]recent [hours][/cyan], [cyan]help[/cyan], [cyan]q[/cyan]"
|
283
|
+
)
|
284
|
+
|
285
|
+
body: Any
|
286
|
+
if state.view == "experiments":
|
287
|
+
df = await _fetch_experiments(db_url)
|
288
|
+
body = _experiments_table(df, state.limit)
|
289
|
+
elif state.view == "experiment" and state.view_arg:
|
290
|
+
detail = await _experiment_detail(db_url, state.view_arg)
|
291
|
+
body = _render_experiment_panel(detail)
|
292
|
+
elif state.view == "usage":
|
293
|
+
body = await _usage_table(db_url, state.view_arg)
|
294
|
+
elif state.view == "traces":
|
295
|
+
body = await _traces_table(db_url, state.limit)
|
296
|
+
elif state.view == "recent":
|
297
|
+
body = await _recent_table(db_url, state.hours, state.limit)
|
298
|
+
else:
|
299
|
+
body = Panel("Unknown view", border_style="red")
|
300
|
+
|
301
|
+
footer_lines = []
|
302
|
+
if state.error:
|
303
|
+
footer_lines.append(f"[red]Error:[/red] {state.error}")
|
304
|
+
if state.last_msg:
|
305
|
+
footer_lines.append(state.last_msg)
|
306
|
+
footer = "\n".join(footer_lines) if footer_lines else help_bar
|
307
|
+
|
308
|
+
# Render a visible input field with an outline and a blinking cursor so users
|
309
|
+
# know where to type commands. This is a visual affordance only; input is
|
310
|
+
# still read from stdin in the background.
|
311
|
+
cursor_char = "█" if state.cursor_on else " "
|
312
|
+
placeholder = "Type a command and press Enter"
|
313
|
+
# Show a dim placeholder when idle; show last message above via subtitle
|
314
|
+
input_text = f"› [dim]{placeholder}[/dim] {cursor_char}"
|
315
|
+
input_panel = Panel(
|
316
|
+
input_text,
|
317
|
+
box=box.SQUARE,
|
318
|
+
border_style="magenta",
|
319
|
+
padding=(0, 1),
|
320
|
+
title="Command",
|
321
|
+
title_align="left",
|
322
|
+
)
|
323
|
+
|
324
|
+
combined = Group(
|
325
|
+
Align.left(body),
|
326
|
+
Align.left(input_panel),
|
327
|
+
)
|
328
|
+
|
329
|
+
return Panel(
|
330
|
+
combined,
|
331
|
+
title=header,
|
332
|
+
border_style="green",
|
333
|
+
subtitle=footer,
|
334
|
+
)
|
335
|
+
|
336
|
+
|
337
|
+
async def _handle_command(cmd: str, state: _State):
|
338
|
+
if not cmd:
|
339
|
+
return
|
340
|
+
parts = cmd.split()
|
341
|
+
c = parts[0].lower()
|
342
|
+
args = parts[1:]
|
343
|
+
|
344
|
+
if c in {":q", "q", "quit", "exit"}:
|
345
|
+
raise KeyboardInterrupt()
|
346
|
+
if c in {"help", ":h", "h", "?"}:
|
347
|
+
state.last_msg = (
|
348
|
+
"experiments | experiment <id> | usage [model] | traces | recent [hours] | q"
|
349
|
+
)
|
350
|
+
return
|
351
|
+
if c in {"experiments", "exp", "e"}:
|
352
|
+
state.view = "experiments"
|
353
|
+
state.view_arg = None
|
354
|
+
state.last_msg = "Showing experiments"
|
355
|
+
return
|
356
|
+
if c in {"experiment", "x"} and args:
|
357
|
+
state.view = "experiment"
|
358
|
+
state.view_arg = args[0]
|
359
|
+
state.last_msg = f"Experiment {args[0]}"
|
360
|
+
return
|
361
|
+
if c in {"usage", "u"}:
|
362
|
+
state.view = "usage"
|
363
|
+
state.view_arg = args[0] if args else None
|
364
|
+
state.last_msg = f"Usage {state.view_arg or ''}"
|
365
|
+
return
|
366
|
+
if c in {"traces", "t"}:
|
367
|
+
state.view = "traces"
|
368
|
+
state.view_arg = None
|
369
|
+
state.last_msg = "Recent sessions"
|
370
|
+
return
|
371
|
+
if c in {"recent", "r"}:
|
372
|
+
hours = _parse_hours(args)
|
373
|
+
if hours is not None:
|
374
|
+
state.hours = hours
|
375
|
+
state.view = "recent"
|
376
|
+
state.last_msg = f"Recent {state.hours:g}h"
|
377
|
+
return
|
378
|
+
|
379
|
+
state.last_msg = f"Unknown command: {cmd}"
|
380
|
+
|
381
|
+
|
382
|
+
def _parse_hours(args: List[str]) -> Optional[float]:
|
383
|
+
if not args:
|
384
|
+
return None
|
385
|
+
# Accept formats: "6", "--6", "-6", "6h"
|
386
|
+
token = args[0]
|
387
|
+
token = token.lstrip("-")
|
388
|
+
if token.endswith("h"):
|
389
|
+
token = token[:-1]
|
390
|
+
try:
|
391
|
+
return float(token)
|
392
|
+
except ValueError:
|
393
|
+
return None
|
394
|
+
|
395
|
+
|
396
|
+
async def _usage_table(db_url: str, model_name: Optional[str]):
|
397
|
+
from synth_ai.tracing_v3.turso.manager import AsyncSQLTraceManager
|
398
|
+
db = AsyncSQLTraceManager(db_url)
|
399
|
+
await db.initialize()
|
400
|
+
try:
|
401
|
+
df = await db.get_model_usage(model_name=model_name)
|
402
|
+
finally:
|
403
|
+
await db.close()
|
404
|
+
|
405
|
+
table = Table(title="Model Usage", header_style="bold", box=box.SIMPLE)
|
406
|
+
if df is None or df.empty:
|
407
|
+
table.add_column("Info")
|
408
|
+
table.add_row("No model usage data found.")
|
409
|
+
return table
|
410
|
+
for col in df.columns:
|
411
|
+
table.add_column(str(col))
|
412
|
+
for row in df.itertuples(index=False):
|
413
|
+
table.add_row(*[str(cell) for cell in row])
|
414
|
+
return table
|
415
|
+
|
416
|
+
|
417
|
+
async def _traces_table(db_url: str, limit: int):
|
418
|
+
from synth_ai.tracing_v3.turso.manager import AsyncSQLTraceManager
|
419
|
+
db = AsyncSQLTraceManager(db_url)
|
420
|
+
await db.initialize()
|
421
|
+
try:
|
422
|
+
df = await db.query_traces("SELECT * FROM session_summary ORDER BY created_at DESC")
|
423
|
+
finally:
|
424
|
+
await db.close()
|
425
|
+
|
426
|
+
table = Table(title="Recent Sessions", box=box.SIMPLE, header_style="bold")
|
427
|
+
for col in ["Session", "Experiment", "Events", "Msgs", "Timesteps", "Cost", "Created"]:
|
428
|
+
table.add_column(col, justify="right" if col in {"Events","Msgs","Timesteps"} else "left")
|
429
|
+
|
430
|
+
if df is None or df.empty:
|
431
|
+
table.add_row("-", "No sessions found", "-", "-", "-", "-", "-")
|
432
|
+
else:
|
433
|
+
count = 0
|
434
|
+
for _, r in df.iterrows():
|
435
|
+
if count >= limit:
|
436
|
+
break
|
437
|
+
count += 1
|
438
|
+
table.add_row(
|
439
|
+
str(r.get("session_id", ""))[:10],
|
440
|
+
str(r.get("experiment_name", ""))[:24],
|
441
|
+
f"{int(r.get('num_events', 0)):,}",
|
442
|
+
f"{int(r.get('num_messages', 0)):,}",
|
443
|
+
f"{int(r.get('num_timesteps', 0)):,}",
|
444
|
+
f"${float(r.get('total_cost_usd', 0.0) or 0.0):.4f}",
|
445
|
+
str(r.get("created_at", "")),
|
446
|
+
)
|
447
|
+
return table
|
448
|
+
|
449
|
+
|
450
|
+
async def _recent_table(db_url: str, hours: float, limit: int):
|
451
|
+
# Inline the recent query to avoid cross-module coupling
|
452
|
+
from datetime import timedelta
|
453
|
+
from synth_ai.tracing_v3.turso.manager import AsyncSQLTraceManager
|
454
|
+
|
455
|
+
start_time = datetime.now() - timedelta(hours=hours)
|
456
|
+
db = AsyncSQLTraceManager(db_url)
|
457
|
+
await db.initialize()
|
458
|
+
try:
|
459
|
+
query = """
|
460
|
+
WITH windowed_sessions AS (
|
461
|
+
SELECT * FROM session_traces WHERE created_at >= :start_time
|
462
|
+
)
|
463
|
+
SELECT
|
464
|
+
e.experiment_id,
|
465
|
+
e.name,
|
466
|
+
MIN(ws.created_at) AS window_start,
|
467
|
+
MAX(ws.created_at) AS window_end,
|
468
|
+
COUNT(DISTINCT ws.session_id) AS runs,
|
469
|
+
COUNT(DISTINCT ev.id) AS events,
|
470
|
+
COUNT(DISTINCT m.id) AS messages,
|
471
|
+
SUM(CASE WHEN ev.event_type = 'cais' THEN ev.cost_usd ELSE 0 END) / 100.0 AS cost_usd,
|
472
|
+
SUM(CASE WHEN ev.event_type = 'cais' THEN ev.total_tokens ELSE 0 END) AS tokens
|
473
|
+
FROM windowed_sessions ws
|
474
|
+
LEFT JOIN experiments e ON ws.experiment_id = e.experiment_id
|
475
|
+
LEFT JOIN events ev ON ws.session_id = ev.session_id
|
476
|
+
LEFT JOIN messages m ON ws.session_id = m.session_id
|
477
|
+
GROUP BY e.experiment_id, e.name
|
478
|
+
ORDER BY window_end DESC
|
479
|
+
"""
|
480
|
+
df = await db.query_traces(query, {"start_time": start_time})
|
481
|
+
finally:
|
482
|
+
await db.close()
|
483
|
+
|
484
|
+
table = Table(title=f"Experiments in last {hours:g}h", header_style="bold", box=box.SIMPLE)
|
485
|
+
for col in ["Experiment", "Runs", "First", "Last", "Events", "Msgs", "Cost", "Tokens"]:
|
486
|
+
table.add_column(col, justify="right" if col in {"Runs","Events","Msgs","Tokens"} else "left")
|
487
|
+
|
488
|
+
if df is None or df.empty:
|
489
|
+
table.add_row("-", "0", "-", "-", "-", "-", "-", "-")
|
490
|
+
else:
|
491
|
+
count = 0
|
492
|
+
for _, r in df.iterrows():
|
493
|
+
if count >= limit:
|
494
|
+
break
|
495
|
+
count += 1
|
496
|
+
name = r.get("name") or "Unnamed"
|
497
|
+
exp_disp = f"{name[:28]} [dim]({(str(r.get('experiment_id',''))[:8])})[/dim]"
|
498
|
+
table.add_row(
|
499
|
+
exp_disp,
|
500
|
+
f"{int(r.get('runs', 0)):,}",
|
501
|
+
str(r.get("window_start", "")),
|
502
|
+
str(r.get("window_end", "")),
|
503
|
+
f"{int(r.get('events', 0)):,}",
|
504
|
+
f"{int(r.get('messages', 0)):,}",
|
505
|
+
f"${float(r.get('cost_usd', 0.0) or 0.0):.4f}",
|
506
|
+
f"{int(r.get('tokens', 0)):,}",
|
507
|
+
)
|
508
|
+
return table
|
@@ -0,0 +1,53 @@
|
|
1
|
+
"""
|
2
|
+
Base URL resolution for learning-v2 and related backend APIs.
|
3
|
+
|
4
|
+
Default to production, allow overrides via environment variables:
|
5
|
+
- LEARNING_V2_BASE_URL (highest precedence)
|
6
|
+
- SYNTH_BASE_URL (legacy)
|
7
|
+
- SYNTH_LOCAL_BASE_URL
|
8
|
+
- SYNTH_DEV_BASE_URL
|
9
|
+
- SYNTH_PROD_BASE_URL (fallback if none provided)
|
10
|
+
|
11
|
+
Normalization: ensure the returned URL ends with "/api".
|
12
|
+
"""
|
13
|
+
|
14
|
+
import os
|
15
|
+
from typing import Literal
|
16
|
+
|
17
|
+
|
18
|
+
PROD_BASE_URL_DEFAULT = "https://agent-learning.onrender.com"
|
19
|
+
|
20
|
+
|
21
|
+
def _normalize_base(url: str) -> str:
|
22
|
+
url = url.strip()
|
23
|
+
if url.endswith("/v1"):
|
24
|
+
url = url[:-3]
|
25
|
+
url = url.rstrip("/")
|
26
|
+
if not url.endswith("/api"):
|
27
|
+
url = f"{url}/api"
|
28
|
+
return url
|
29
|
+
|
30
|
+
|
31
|
+
def get_learning_v2_base_url(mode: Literal["dev","prod"] = "prod") -> str:
|
32
|
+
if mode == "prod":
|
33
|
+
prod = os.getenv("SYNTH_PROD_BASE_URL") or PROD_BASE_URL_DEFAULT
|
34
|
+
return _normalize_base(prod)
|
35
|
+
# Priority order
|
36
|
+
env_url = os.getenv("LEARNING_V2_BASE_URL")
|
37
|
+
if env_url:
|
38
|
+
return _normalize_base(env_url)
|
39
|
+
|
40
|
+
legacy = os.getenv("SYNTH_BASE_URL")
|
41
|
+
if legacy:
|
42
|
+
return _normalize_base(legacy)
|
43
|
+
|
44
|
+
local = os.getenv("SYNTH_LOCAL_BASE_URL")
|
45
|
+
if local:
|
46
|
+
return _normalize_base(local)
|
47
|
+
|
48
|
+
dev = os.getenv("SYNTH_DEV_BASE_URL")
|
49
|
+
if dev:
|
50
|
+
return _normalize_base(dev)
|
51
|
+
|
52
|
+
raise Exception()
|
53
|
+
|