tavus-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.
tavus_mcp/cli/main.py ADDED
@@ -0,0 +1,1467 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import json
5
+ import os
6
+ import platform
7
+ from pathlib import Path
8
+ from typing import Any
9
+
10
+ import typer
11
+ from rich.console import Console
12
+ from rich.table import Table
13
+
14
+ from tavus_mcp.sdk.auth import keyring_store
15
+ from tavus_mcp.sdk.auth.oauth import login_with_browser
16
+ from tavus_mcp.sdk.auth.session import get_session
17
+ from tavus_mcp.sdk.client.http import TavusClient
18
+ from tavus_mcp.sdk.env import load_config
19
+ from tavus_mcp.sdk.errors import TavusError
20
+ from tavus_mcp.sdk.patch import allowed_patch_paths, validate_patch_operations
21
+ from tavus_mcp.sdk.recipes.build_and_verify import build_and_verify as build_and_verify_recipe
22
+ from tavus_mcp.sdk.recipes.options import describe_persona_options
23
+ from tavus_mcp.sdk.recipes.quickstart import quickstart as quickstart_recipe
24
+ from tavus_mcp.sdk.recipes.scaffold_embed import scaffold_embed
25
+
26
+ HELP_CONTEXT = {"help_option_names": ["-h", "--help"]}
27
+
28
+ app = typer.Typer(no_args_is_help=True, context_settings=HELP_CONTEXT)
29
+ auth_app = typer.Typer(no_args_is_help=True, context_settings=HELP_CONTEXT)
30
+ persona_app = typer.Typer(no_args_is_help=True, context_settings=HELP_CONTEXT)
31
+ replica_app = typer.Typer(no_args_is_help=True, context_settings=HELP_CONTEXT)
32
+ conversation_app = typer.Typer(no_args_is_help=True, context_settings=HELP_CONTEXT)
33
+ resource_app = typer.Typer(no_args_is_help=True, context_settings=HELP_CONTEXT)
34
+ guardrail_app = typer.Typer(no_args_is_help=True, context_settings=HELP_CONTEXT)
35
+ objective_app = typer.Typer(no_args_is_help=True, context_settings=HELP_CONTEXT)
36
+ tool_app = typer.Typer(no_args_is_help=True, context_settings=HELP_CONTEXT)
37
+ pronunciation_app = typer.Typer(no_args_is_help=True, context_settings=HELP_CONTEXT)
38
+ persona_tools_app = typer.Typer(no_args_is_help=True, context_settings=HELP_CONTEXT)
39
+ builder_app = typer.Typer(no_args_is_help=True, context_settings=HELP_CONTEXT)
40
+ builder_update_app = typer.Typer(no_args_is_help=True, context_settings=HELP_CONTEXT)
41
+ chat_app = typer.Typer(no_args_is_help=True, context_settings=HELP_CONTEXT)
42
+ app.add_typer(auth_app, name="auth")
43
+ app.add_typer(persona_app, name="persona")
44
+ persona_app.add_typer(persona_tools_app, name="tools")
45
+ app.add_typer(replica_app, name="replica")
46
+ app.add_typer(conversation_app, name="conversation")
47
+ app.add_typer(resource_app, name="resource")
48
+ app.add_typer(guardrail_app, name="guardrail")
49
+ app.add_typer(objective_app, name="objective")
50
+ app.add_typer(tool_app, name="tool")
51
+ app.add_typer(pronunciation_app, name="pronunciation-dictionary")
52
+ app.add_typer(builder_app, name="builder")
53
+ builder_app.add_typer(builder_update_app, name="update")
54
+ app.add_typer(chat_app, name="chat")
55
+ console = Console()
56
+
57
+
58
+ @app.callback()
59
+ def main(
60
+ env: str | None = typer.Option(
61
+ None,
62
+ "--env",
63
+ "-e",
64
+ help="Target Tavus environment. Defaults to PROD; use TEST for the test DB.",
65
+ ),
66
+ ) -> None:
67
+ """Tavus CLI."""
68
+ if env:
69
+ os.environ["TAVUS_ENV"] = env
70
+
71
+
72
+ def _run(coro: Any) -> Any:
73
+ try:
74
+ return asyncio.run(coro)
75
+ except TavusError as error:
76
+ console.print(f"[red]{error}[/red]")
77
+ raise typer.Exit(1) from error
78
+
79
+
80
+ def _json(data: Any) -> None:
81
+ console.print_json(json.dumps(data, default=str))
82
+
83
+
84
+ def _parse_value(value: str) -> Any:
85
+ try:
86
+ return json.loads(value)
87
+ except json.JSONDecodeError:
88
+ return value
89
+
90
+
91
+ @app.command()
92
+ def doctor(
93
+ skip_network: bool = typer.Option(False, help="Only validate local configuration."),
94
+ ) -> None:
95
+ """Show selected environment, auth source, and API reachability."""
96
+ config = load_config()
97
+ session = get_session(config, required=False)
98
+ table = Table(title="Tavus MCP Doctor")
99
+ table.add_column("Key")
100
+ table.add_column("Value")
101
+ table.add_row("env", config.env.value)
102
+ table.add_row("env_file", str(config.env_file) if config.env_file else "(none)")
103
+ table.add_row("public_api_base_url", str(config.public_api_base_url))
104
+ table.add_row("portal_api_base_url", str(config.portal_api_base_url))
105
+ table.add_row("dev_portal_url", str(config.dev_portal_url))
106
+ table.add_row("auth_source", session.source if session else "(missing)")
107
+ console.print(table)
108
+
109
+ if skip_network:
110
+ return
111
+ if not session:
112
+ raise typer.Exit(1)
113
+
114
+ async def check() -> None:
115
+ async with TavusClient(config, session) as client:
116
+ await client.personas.list(limit=1)
117
+ await client.replicas.list(limit=1)
118
+
119
+ _run(check())
120
+ console.print("[green]Network check passed.[/green]")
121
+
122
+
123
+ @auth_app.command("login")
124
+ def auth_login(
125
+ api_key: str | None = typer.Option(
126
+ None,
127
+ help="Store an existing Tavus API key instead of opening Firebase login.",
128
+ ),
129
+ name: str | None = typer.Option(None, help="Name for the minted API key."),
130
+ ) -> None:
131
+ """Log in with Firebase and store an env-scoped Tavus API key."""
132
+ config = load_config()
133
+ if api_key:
134
+ keyring_store.set_api_key(config, api_key)
135
+ console.print(f"Stored API key for {config.env.value}.")
136
+ return
137
+
138
+ key_name = name or f"tavus-cli ({platform.node() or 'unknown-host'} {config.env.value})"
139
+ result = _run(login_with_browser(config, key_name=key_name))
140
+ console.print(f"Logged in as {result.email or 'Tavus user'} for {config.env.value}.")
141
+
142
+
143
+ @auth_app.command("logout")
144
+ def auth_logout() -> None:
145
+ """Delete the env-scoped API key from local keyring."""
146
+ config = load_config()
147
+ keyring_store.delete_api_key(config)
148
+ console.print(f"Deleted keyring API key for {config.env.value}.")
149
+
150
+
151
+ @auth_app.command("status")
152
+ def auth_status() -> None:
153
+ """Show whether credentials are available for the selected env."""
154
+ config = load_config()
155
+ session = get_session(config, required=False)
156
+ _json(
157
+ {
158
+ "env": config.env.value,
159
+ "authenticated": session is not None,
160
+ "source": session.source if session else None,
161
+ }
162
+ )
163
+
164
+
165
+ @persona_app.command("list")
166
+ def persona_list(
167
+ limit: int = typer.Option(25),
168
+ page: int = typer.Option(0),
169
+ persona_type: str | None = typer.Option(None),
170
+ json_output: bool = typer.Option(False, "--json"),
171
+ ) -> None:
172
+ """List personas for the selected env/account."""
173
+
174
+ async def run() -> Any:
175
+ async with TavusClient.from_env() as client:
176
+ return await client.personas.list(limit=limit, page=page, persona_type=persona_type)
177
+
178
+ data = _run(run())
179
+ if json_output:
180
+ _json(data)
181
+ return
182
+ _print_rows(data, ["persona_id", "persona_name", "default_replica_id", "updated_at"])
183
+
184
+
185
+ @persona_app.command("get")
186
+ def persona_get(persona_id: str, include_settings: bool = typer.Option(False)) -> None:
187
+ """Get a persona as JSON."""
188
+
189
+ async def run() -> Any:
190
+ async with TavusClient.from_env() as client:
191
+ return await client.personas.get(
192
+ persona_id,
193
+ include_settings=str(include_settings).lower(),
194
+ )
195
+
196
+ _json(_run(run()))
197
+
198
+
199
+ @persona_app.command("create")
200
+ def persona_create(
201
+ system_prompt: str | None = typer.Option(None, "--system-prompt"),
202
+ name: str = typer.Option("Agentic Tavus Persona", "--name"),
203
+ replica_id: str | None = typer.Option(None),
204
+ pipeline_mode: str = typer.Option(
205
+ "full", "--pipeline-mode", help="full | speech-to-speech | echo"
206
+ ),
207
+ greeting: str | None = typer.Option(None, "--greeting"),
208
+ context: str | None = typer.Option(None, "--context"),
209
+ layers_file: Path | None = typer.Option(
210
+ None, "--layers-file", help="JSON file with layer overrides."
211
+ ),
212
+ memories: list[str] | None = typer.Option(
213
+ None, "--memory", help="Initial seed memory. Repeat for multiple."
214
+ ),
215
+ objectives_id: str | None = typer.Option(None, "--objectives-id"),
216
+ guardrails_id: str | None = typer.Option(
217
+ None, "--guardrails-id", help="Legacy guardrail-set id."
218
+ ),
219
+ guardrail_ids: list[str] | None = typer.Option(
220
+ None, "--guardrail-id", help="Repeat for multiple. New flat shape."
221
+ ),
222
+ guardrail_tags: list[str] | None = typer.Option(
223
+ None, "--guardrail-tag", help="Repeat for multiple. New flat shape."
224
+ ),
225
+ document_ids: list[str] | None = typer.Option(
226
+ None, "--document-id", help="Repeat for multiple."
227
+ ),
228
+ document_tags: list[str] | None = typer.Option(
229
+ None, "--document-tag", help="Repeat for multiple."
230
+ ),
231
+ is_template: bool = typer.Option(
232
+ False, "--template/--no-template", help="Flag as a clonable template."
233
+ ),
234
+ ) -> None:
235
+ """Create a persona. ``--system-prompt`` is required for the default
236
+ ``--pipeline-mode=full``; omit it for ``speech-to-speech`` or ``echo``.
237
+ Attach tools post-create with ``tavus persona tools attach``."""
238
+
239
+ async def run() -> Any:
240
+ async with TavusClient.from_env() as client:
241
+ body: dict[str, Any] = {"persona_name": name}
242
+ if pipeline_mode and pipeline_mode != "full":
243
+ body["pipeline_mode"] = pipeline_mode
244
+ if system_prompt is not None:
245
+ body["system_prompt"] = system_prompt
246
+ if replica_id:
247
+ body["default_replica_id"] = replica_id
248
+ if greeting is not None:
249
+ body["greeting"] = greeting
250
+ if context is not None:
251
+ body["context"] = context
252
+ if layers_file:
253
+ body["layers"] = json.loads(layers_file.read_text())
254
+ if memories:
255
+ body["memories"] = list(memories)
256
+ if objectives_id:
257
+ body["objectives_id"] = objectives_id
258
+ if guardrails_id:
259
+ body["guardrails_id"] = guardrails_id
260
+ if guardrail_ids:
261
+ body["guardrail_ids"] = list(guardrail_ids)
262
+ if guardrail_tags:
263
+ body["guardrail_tags"] = list(guardrail_tags)
264
+ if document_ids:
265
+ body["document_ids"] = list(document_ids)
266
+ if document_tags:
267
+ body["document_tags"] = list(document_tags)
268
+ if is_template:
269
+ body["is_template"] = True
270
+ return await client.personas.create(body)
271
+
272
+ _json(_run(run()))
273
+
274
+
275
+ @persona_app.command("delete")
276
+ def persona_delete(persona_id: str) -> None:
277
+ """Delete a persona."""
278
+
279
+ async def run() -> Any:
280
+ async with TavusClient.from_env() as client:
281
+ return await client.personas.delete(persona_id)
282
+
283
+ _json(_run(run()))
284
+
285
+
286
+ @persona_app.command("patch")
287
+ def persona_patch(
288
+ persona_id: str,
289
+ op: str = typer.Option("replace", help="JSON Patch op."),
290
+ path: str = typer.Option(..., help="JSON Pointer path, e.g. /persona_name."),
291
+ value: str | None = typer.Option(None, help="JSON value; strings may be passed as plain text."),
292
+ patch_file: Path | None = typer.Option(None, help="JSON patch file. Overrides op/path/value."),
293
+ dry_run: bool = typer.Option(False, help="Validate but do not send."),
294
+ ) -> None:
295
+ """Apply a validated JSON Patch to a persona."""
296
+ if patch_file:
297
+ ops = json.loads(patch_file.read_text())
298
+ else:
299
+ item: dict[str, Any] = {"op": op, "path": path}
300
+ if value is not None:
301
+ item["value"] = _parse_value(value)
302
+ ops = [item]
303
+ validated = validate_patch_operations(ops)
304
+ if dry_run:
305
+ _json({"valid": True, "ops": validated})
306
+ return
307
+
308
+ async def run() -> Any:
309
+ async with TavusClient.from_env() as client:
310
+ return await client.personas.patch(persona_id, validated)
311
+
312
+ _json(_run(run()))
313
+
314
+
315
+ @persona_app.command("options")
316
+ def persona_options(persona_id: str) -> None:
317
+ """Return the persona plus valid account resources and patchable paths."""
318
+
319
+ async def run() -> Any:
320
+ async with TavusClient.from_env() as client:
321
+ return await describe_persona_options(client, persona_id)
322
+
323
+ _json(_run(run()))
324
+
325
+
326
+ @persona_app.command("paths")
327
+ def persona_paths() -> None:
328
+ """List locally known JSON Patch paths for personas."""
329
+ _json(sorted(allowed_patch_paths()))
330
+
331
+
332
+ @persona_app.command("build")
333
+ def persona_build(
334
+ prompt: str | None = typer.Option(
335
+ None,
336
+ "--prompt",
337
+ help="Initial creator prompt, e.g. 'I want to make a persona for an office greeter...'.",
338
+ ),
339
+ goal: str | None = typer.Option(None, "--goal", hidden=True),
340
+ replica_id: str | None = typer.Option(
341
+ None,
342
+ "--replica-id",
343
+ help="Override the default replica selected by the builder backend.",
344
+ ),
345
+ max_rounds: int = typer.Option(
346
+ 4,
347
+ "--max-rounds",
348
+ min=1,
349
+ help="Maximum builder question/answer rounds before publishing.",
350
+ ),
351
+ json_output: bool = typer.Option(False, "--json"),
352
+ answer: list[str] | None = typer.Option(None, "--answer", hidden=True),
353
+ model: str = typer.Option("claude-sonnet-4-6", "--model", hidden=True),
354
+ inner_model: str | None = typer.Option(None, "--inner-model", hidden=True),
355
+ ) -> None:
356
+ """Build a persona through the same conversational builder flow used by
357
+ creator-studio, then verify the published persona through CVI chat mode."""
358
+
359
+ creator_prompt = (prompt or goal or "").strip()
360
+ if not creator_prompt:
361
+ raise typer.BadParameter("Provide --prompt.", param_hint="--prompt")
362
+
363
+ io_console = Console(stderr=json_output)
364
+
365
+ def print_builder_reply(
366
+ reply: dict[str, Any], round_number: int, _transcript: list[dict[str, Any]]
367
+ ) -> None:
368
+ text = str(reply.get("text") or "").strip()
369
+ if text:
370
+ io_console.print(f"\n[bold]Builder[/bold] [{round_number}/{max_rounds}]: {text}")
371
+ suggestions = [str(item) for item in (reply.get("suggestions") or []) if item]
372
+ if suggestions:
373
+ io_console.print("[dim]Suggestions:[/dim] " + " | ".join(suggestions))
374
+
375
+ def ask_creator(
376
+ _reply: dict[str, Any], _round_number: int, _transcript: list[dict[str, Any]]
377
+ ) -> str | None:
378
+ try:
379
+ value = io_console.input("[bold]You[/bold] (blank to finish): ")
380
+ except (EOFError, KeyboardInterrupt):
381
+ return None
382
+ return value.strip() or None
383
+
384
+ async def run() -> Any:
385
+ async with TavusClient.from_env() as client:
386
+ return await build_and_verify_recipe(
387
+ client,
388
+ prompt=creator_prompt,
389
+ replica_id=replica_id,
390
+ model=model,
391
+ inner_model=inner_model,
392
+ max_rounds=max_rounds,
393
+ answers=list(answer) if answer else None,
394
+ answer_provider=None if answer else ask_creator,
395
+ on_builder_reply=print_builder_reply,
396
+ auto_refine=True,
397
+ max_refine_rounds=1,
398
+ )
399
+
400
+ data = _run(run())
401
+ if json_output:
402
+ _json(data)
403
+ return
404
+ verdict = data.get("verdict") or {}
405
+ console.print(f"[bold]builder_id[/bold]: {data.get('builder_id')}")
406
+ console.print(f"[bold]persona_id[/bold]: {data.get('persona_id')}")
407
+ if data.get("replica_id"):
408
+ console.print(f"[bold]replica_id[/bold]: {data.get('replica_id')}")
409
+ if data.get("persona_url"):
410
+ console.print(f"[bold]persona_url[/bold]: {data.get('persona_url')}")
411
+ console.print(f"[bold]overall[/bold]: {verdict.get('overall', '?')}")
412
+ if data.get("refine_rounds_used"):
413
+ console.print(f"[dim]refine_rounds_used={data['refine_rounds_used']}[/dim]")
414
+ summary = verdict.get("summary")
415
+ if summary:
416
+ console.print(f"\n{summary}")
417
+ if data.get("system_prompt"):
418
+ console.print("\n[bold]system_prompt[/bold]:")
419
+ console.print(data["system_prompt"])
420
+
421
+
422
+ @persona_app.command("preview")
423
+ def persona_preview(
424
+ persona_id: str,
425
+ replica_id: str | None = typer.Option(None),
426
+ name: str | None = typer.Option(None, "--name"),
427
+ json_output: bool = typer.Option(False, "--json"),
428
+ ) -> None:
429
+ """Start a full preview conversation against a persona and print the
430
+ conversation_url so a human can verify visually."""
431
+
432
+ async def run() -> Any:
433
+ async with TavusClient.from_env() as client:
434
+ body: dict[str, Any] = {"persona_id": persona_id}
435
+ if replica_id:
436
+ body["replica_id"] = replica_id
437
+ else:
438
+ persona = await client.personas.get(persona_id)
439
+ default_replica = (
440
+ persona.get("default_replica_id") if isinstance(persona, dict) else None
441
+ )
442
+ if default_replica:
443
+ body["replica_id"] = default_replica
444
+ if name:
445
+ body["conversation_name"] = name
446
+ return await client.conversations.create(body)
447
+
448
+ data = _run(run())
449
+ if json_output:
450
+ _json(data)
451
+ return
452
+ url = data.get("conversation_url") if isinstance(data, dict) else None
453
+ if url:
454
+ console.print(url)
455
+ else:
456
+ _json(data)
457
+
458
+
459
+ @replica_app.command("list")
460
+ def replica_list(
461
+ limit: int = typer.Option(25),
462
+ stock: bool = typer.Option(False, "--stock"),
463
+ json_output: bool = typer.Option(False, "--json"),
464
+ ) -> None:
465
+ """List replicas."""
466
+
467
+ async def run() -> Any:
468
+ async with TavusClient.from_env() as client:
469
+ return await client.replicas.list(limit=limit, replica_type="system" if stock else None)
470
+
471
+ data = _run(run())
472
+ if json_output:
473
+ _json(data)
474
+ return
475
+ _print_rows(data, ["replica_id", "replica_name", "status", "model_name"])
476
+
477
+
478
+ @conversation_app.command("create")
479
+ def conversation_create(
480
+ persona_id: str | None = typer.Option(None),
481
+ replica_id: str | None = typer.Option(None),
482
+ name: str | None = typer.Option(None),
483
+ ) -> None:
484
+ """Create a conversation."""
485
+
486
+ async def run() -> Any:
487
+ body = {"persona_id": persona_id, "replica_id": replica_id, "conversation_name": name}
488
+ async with TavusClient.from_env() as client:
489
+ return await client.conversations.create({k: v for k, v in body.items() if v})
490
+
491
+ _json(_run(run()))
492
+
493
+
494
+ @conversation_app.command("end")
495
+ def conversation_end(conversation_id: str) -> None:
496
+ """End a conversation."""
497
+
498
+ async def run() -> Any:
499
+ async with TavusClient.from_env() as client:
500
+ return await client.conversations.end(conversation_id)
501
+
502
+ _json(_run(run()))
503
+
504
+
505
+ @builder_app.command("create")
506
+ def builder_create(
507
+ name: str = typer.Option(..., "--name"),
508
+ greeting: str | None = typer.Option(None),
509
+ persona_id: str | None = typer.Option(None),
510
+ model: str | None = typer.Option(None, help="claude or gpt-oss-120b"),
511
+ ) -> None:
512
+ """Start a builder session. The CLI mirrors the dev-portal creator-studio
513
+ flow — give the session a name and optionally seed a greeting or attach
514
+ an existing persona."""
515
+
516
+ async def run() -> Any:
517
+ body: dict[str, Any] = {"name": name}
518
+ if greeting is not None:
519
+ body["greeting"] = greeting
520
+ if persona_id:
521
+ body["persona_id"] = persona_id
522
+ if model:
523
+ body["model"] = model
524
+ async with TavusClient.from_env() as client:
525
+ return await client.builders.create(body)
526
+
527
+ _json(_run(run()))
528
+
529
+
530
+ @builder_app.command("list")
531
+ def builder_list(
532
+ limit: int | None = typer.Option(None),
533
+ page: int | None = typer.Option(None),
534
+ persona_id: str | None = typer.Option(None),
535
+ name: str | None = typer.Option(None),
536
+ status: str | None = typer.Option(None),
537
+ json_output: bool = typer.Option(False, "--json"),
538
+ ) -> None:
539
+ """List builder sessions for the authenticated account."""
540
+
541
+ async def run() -> Any:
542
+ async with TavusClient.from_env() as client:
543
+ return await client.builders.list(
544
+ limit=limit, page=page, persona_id=persona_id, name=name, status=status
545
+ )
546
+
547
+ data = _run(run())
548
+ if json_output:
549
+ _json(data)
550
+ return
551
+ sessions = data.get("sessions", data) if isinstance(data, dict) else data
552
+ if not isinstance(sessions, list):
553
+ _json(data)
554
+ return
555
+ columns = ("uuid", "name", "persona_id", "status", "updated_at")
556
+ table = Table()
557
+ for column in columns:
558
+ table.add_column(column)
559
+ for row in sessions:
560
+ table.add_row(*[str(row.get(column, "")) for column in columns])
561
+ console.print(table)
562
+
563
+
564
+ @builder_app.command("get")
565
+ def builder_get(builder_id: str) -> None:
566
+ """Fetch a single builder session."""
567
+
568
+ async def run() -> Any:
569
+ async with TavusClient.from_env() as client:
570
+ return await client.builders.get(builder_id)
571
+
572
+ _json(_run(run()))
573
+
574
+
575
+ @builder_app.command("delete")
576
+ def builder_delete(builder_id: str) -> None:
577
+ """Soft-delete a builder session."""
578
+
579
+ async def run() -> Any:
580
+ async with TavusClient.from_env() as client:
581
+ return await client.builders.delete(builder_id)
582
+
583
+ _json(_run(run()))
584
+
585
+
586
+ @builder_app.command("chat")
587
+ def builder_chat(
588
+ builder_id: str,
589
+ message: str = typer.Option(..., "--message"),
590
+ json_output: bool = typer.Option(False, "--json"),
591
+ ) -> None:
592
+ """Send a chat turn to the builder. Prints the assistant text, any
593
+ suggestion chips, then a footer with ``draft_ready`` and the targets the
594
+ builder LLM signaled (use ``tavus builder update <target> ...`` next)."""
595
+
596
+ async def run() -> Any:
597
+ async with TavusClient.from_env() as client:
598
+ return await client.builders.chat(builder_id, message)
599
+
600
+ data = _run(run())
601
+ if json_output:
602
+ _json(data)
603
+ return
604
+ text = data.get("text") or ""
605
+ suggestions = data.get("suggestions") or []
606
+ targets = data.get("targets") or []
607
+ draft_ready = data.get("draft_ready", False)
608
+ if text:
609
+ console.print(text)
610
+ if suggestions:
611
+ console.print("\n[bold]Suggestions[/bold]")
612
+ for i, suggestion in enumerate(suggestions, 1):
613
+ console.print(f" {i}. {suggestion}")
614
+ console.print(f"\n[dim]draft_ready={draft_ready} targets={targets or '[]'}[/dim]")
615
+
616
+
617
+ @builder_app.command("history")
618
+ def builder_history(
619
+ builder_id: str,
620
+ limit: int = typer.Option(50),
621
+ json_output: bool = typer.Option(False, "--json"),
622
+ ) -> None:
623
+ """Print the chat transcript for a builder session."""
624
+
625
+ async def run() -> Any:
626
+ async with TavusClient.from_env() as client:
627
+ return await client.builders.chat_history(builder_id, limit=limit)
628
+
629
+ data = _run(run())
630
+ if json_output:
631
+ _json(data)
632
+ return
633
+ messages = data.get("messages", data) if isinstance(data, dict) else data
634
+ if not isinstance(messages, list):
635
+ _json(data)
636
+ return
637
+ for entry in messages:
638
+ role = entry.get("role", "?")
639
+ content = entry.get("content", "")
640
+ console.print(f"[bold]{role}[/bold]: {content}")
641
+
642
+
643
+ @builder_app.command("append-messages")
644
+ def builder_append_messages(
645
+ builder_id: str,
646
+ file: Path = typer.Option(..., "--file", help="JSON file with a messages array."),
647
+ ) -> None:
648
+ """Append raw {role, content} messages to a session without invoking the LLM."""
649
+ messages = json.loads(file.read_text())
650
+
651
+ async def run() -> Any:
652
+ async with TavusClient.from_env() as client:
653
+ return await client.builders.append_messages(builder_id, messages)
654
+
655
+ _json(_run(run()))
656
+
657
+
658
+ @builder_app.command("publish")
659
+ def builder_publish(builder_id: str) -> None:
660
+ """Mark a builder session complete and publish its persona."""
661
+
662
+ async def run() -> Any:
663
+ async with TavusClient.from_env() as client:
664
+ return await client.builders.publish(builder_id)
665
+
666
+ _json(_run(run()))
667
+
668
+
669
+ @builder_update_app.command("objectives")
670
+ def builder_update_objectives(
671
+ builder_id: str,
672
+ message: str = typer.Option(..., "--message"),
673
+ ) -> None:
674
+ """Run an LLM update on the persona's objectives list."""
675
+
676
+ async def run() -> Any:
677
+ async with TavusClient.from_env() as client:
678
+ return await client.builders.update_objectives(builder_id, message)
679
+
680
+ _json(_run(run()))
681
+
682
+
683
+ @builder_update_app.command("guardrails")
684
+ def builder_update_guardrails(
685
+ builder_id: str,
686
+ message: str = typer.Option(..., "--message"),
687
+ ) -> None:
688
+ """Run an LLM update on the persona's guardrails list."""
689
+
690
+ async def run() -> Any:
691
+ async with TavusClient.from_env() as client:
692
+ return await client.builders.update_guardrails(builder_id, message)
693
+
694
+ _json(_run(run()))
695
+
696
+
697
+ @builder_update_app.command("greeting")
698
+ def builder_update_greeting(
699
+ builder_id: str,
700
+ message: str = typer.Option(..., "--message"),
701
+ ) -> None:
702
+ """Run an LLM refinement of the persona's greeting."""
703
+
704
+ async def run() -> Any:
705
+ async with TavusClient.from_env() as client:
706
+ return await client.builders.update_greeting(builder_id, message)
707
+
708
+ _json(_run(run()))
709
+
710
+
711
+ @builder_update_app.command("personality")
712
+ def builder_update_personality(
713
+ builder_id: str,
714
+ message: str = typer.Option(..., "--message"),
715
+ persona_name: bool = typer.Option(False, "--name", help="Also update persona_name."),
716
+ system_prompt: bool = typer.Option(False, "--system-prompt", help="Also update system_prompt."),
717
+ ) -> None:
718
+ """Run an LLM refinement of persona name and/or system prompt."""
719
+
720
+ async def run() -> Any:
721
+ async with TavusClient.from_env() as client:
722
+ return await client.builders.update_personality(
723
+ builder_id,
724
+ message,
725
+ persona_name=persona_name,
726
+ system_prompt=system_prompt,
727
+ )
728
+
729
+ _json(_run(run()))
730
+
731
+
732
+ @chat_app.command("start")
733
+ def chat_start(
734
+ persona_id: str = typer.Option(..., "--persona-id"),
735
+ greeting: str | None = typer.Option(None),
736
+ name: str | None = typer.Option(None, "--name"),
737
+ ) -> None:
738
+ """Start a text-only conversation against a persona."""
739
+
740
+ async def run() -> Any:
741
+ body: dict[str, Any] = {"persona_id": persona_id, "chat": True}
742
+ if greeting:
743
+ body["custom_greeting"] = greeting
744
+ if name:
745
+ body["conversation_name"] = name
746
+ async with TavusClient.from_env() as client:
747
+ return await client.conversations.create(body)
748
+
749
+ _json(_run(run()))
750
+
751
+
752
+ @chat_app.command("turn")
753
+ def chat_turn(
754
+ conversation_id: str,
755
+ message: str = typer.Option(..., "--message"),
756
+ timeout: float = typer.Option(20.0, "--timeout"),
757
+ ) -> None:
758
+ """Send one user turn and print the persona's reply."""
759
+
760
+ async def run() -> Any:
761
+ async with TavusClient.from_env() as client:
762
+ return await client.conversations.chat_turn(
763
+ conversation_id, message, timeout_s=timeout
764
+ )
765
+
766
+ console.print(_run(run()))
767
+
768
+
769
+ @chat_app.command("end")
770
+ def chat_end(conversation_id: str) -> None:
771
+ """End a chat-mode conversation."""
772
+
773
+ async def run() -> Any:
774
+ async with TavusClient.from_env() as client:
775
+ return await client.conversations.end(conversation_id)
776
+
777
+ _json(_run(run()))
778
+
779
+
780
+ @app.command()
781
+ def quickstart(
782
+ system_prompt: str = typer.Option(...),
783
+ name: str = typer.Option("Agentic Tavus Persona", "--name"),
784
+ replica_id: str | None = typer.Option(None),
785
+ ) -> None:
786
+ """Create a persona and conversation using a stock replica by default."""
787
+
788
+ async def run() -> Any:
789
+ async with TavusClient.from_env() as client:
790
+ return await quickstart_recipe(
791
+ client,
792
+ system_prompt=system_prompt,
793
+ persona_name=name,
794
+ replica_id=replica_id,
795
+ )
796
+
797
+ _json(_run(run()))
798
+
799
+
800
+ @app.command()
801
+ def embed(
802
+ conversation_url: str = typer.Option(...),
803
+ target: str = typer.Option("iframe"),
804
+ write: bool = typer.Option(False, help="Write files. Default prints manifest only."),
805
+ ) -> None:
806
+ """Create an embed file manifest, optionally writing it locally."""
807
+ manifest = scaffold_embed(conversation_url=conversation_url, target=target) # type: ignore[arg-type]
808
+ if not write:
809
+ _json(manifest.model_dump())
810
+ return
811
+ for entry in manifest.files:
812
+ path = Path(entry.path)
813
+ path.parent.mkdir(parents=True, exist_ok=True)
814
+ path.write_text(entry.content)
815
+ console.print(f"Wrote {path}")
816
+
817
+
818
+ @resource_app.command("list")
819
+ def resource_list(resource: str, limit: int = typer.Option(25)) -> None:
820
+ """List a supported resource: guardrails, objectives, documents, voices, tools."""
821
+
822
+ async def run() -> Any:
823
+ async with TavusClient.from_env() as client:
824
+ selected = _resource(client, resource)
825
+ return await selected.list(limit=limit)
826
+
827
+ _json(_run(run()))
828
+
829
+
830
+ @resource_app.command("get")
831
+ def resource_get(resource: str, resource_id: str) -> None:
832
+ """Get a supported resource."""
833
+
834
+ async def run() -> Any:
835
+ async with TavusClient.from_env() as client:
836
+ selected = _resource(client, resource)
837
+ return await selected.get(resource_id)
838
+
839
+ _json(_run(run()))
840
+
841
+
842
+ @guardrail_app.command("list")
843
+ def guardrail_list(
844
+ limit: int = typer.Option(25),
845
+ page: int = typer.Option(1),
846
+ type: str = typer.Option("user", help="user | system | all"),
847
+ name_or_uuid: str | None = typer.Option(None),
848
+ tags: str | None = typer.Option(None, help="Comma-separated tag filter."),
849
+ legacy: bool = typer.Option(False, help="Return the legacy set-shape list."),
850
+ verbose: bool = typer.Option(False, help="Include persona_refs + guardrail_type."),
851
+ json_output: bool = typer.Option(False, "--json"),
852
+ ) -> None:
853
+ """List guardrails. Defaults to the new flat (per-rule) shape."""
854
+
855
+ async def run() -> Any:
856
+ params: dict[str, Any] = {
857
+ "limit": limit,
858
+ "page": page,
859
+ "type": type,
860
+ "legacy": "true" if legacy else "false",
861
+ }
862
+ if name_or_uuid:
863
+ params["name_or_uuid"] = name_or_uuid
864
+ if tags:
865
+ params["tags"] = tags
866
+ if verbose:
867
+ params["verbose"] = "true"
868
+ async with TavusClient.from_env() as client:
869
+ return await client.guardrails.list(**params)
870
+
871
+ data = _run(run())
872
+ if json_output:
873
+ _json(data)
874
+ return
875
+ _print_rows(
876
+ data, ["uuid", "guardrail_name", "modality", "tags", "updated_at"]
877
+ )
878
+
879
+
880
+ @guardrail_app.command("get")
881
+ def guardrail_get(
882
+ guardrail_id: str,
883
+ verbose: bool = typer.Option(False),
884
+ legacy: bool | None = typer.Option(None),
885
+ ) -> None:
886
+ async def run() -> Any:
887
+ params: dict[str, Any] = {}
888
+ if verbose:
889
+ params["verbose"] = "true"
890
+ if legacy is not None:
891
+ params["legacy"] = "true" if legacy else "false"
892
+ async with TavusClient.from_env() as client:
893
+ return await client.guardrails.get(guardrail_id, **params)
894
+
895
+ _json(_run(run()))
896
+
897
+
898
+ @guardrail_app.command("create")
899
+ def guardrail_create(
900
+ name: str = typer.Option(..., "--name", help="guardrail_name"),
901
+ prompt: str = typer.Option(..., "--prompt", help="guardrail_prompt"),
902
+ modality: str = typer.Option("verbal", help="verbal | visual | audio"),
903
+ callback_url: str = typer.Option(""),
904
+ tool_call: str | None = typer.Option(None, help="JSON object."),
905
+ app_message: bool = typer.Option(True, "--app-message/--no-app-message"),
906
+ tags: list[str] | None = typer.Option(None, "--tag", help="Repeat for multiple."),
907
+ ) -> None:
908
+ """Create a flat (non-set) guardrail."""
909
+
910
+ async def run() -> Any:
911
+ body: dict[str, Any] = {
912
+ "guardrail_name": name,
913
+ "guardrail_prompt": prompt,
914
+ "modality": modality,
915
+ "callback_url": callback_url,
916
+ "app_message": app_message,
917
+ "tags": list(tags) if tags else [],
918
+ }
919
+ if tool_call:
920
+ body["tool_call"] = json.loads(tool_call)
921
+ async with TavusClient.from_env() as client:
922
+ return await client.guardrails.create(body)
923
+
924
+ _json(_run(run()))
925
+
926
+
927
+ @guardrail_app.command("patch")
928
+ def guardrail_patch(
929
+ guardrail_id: str,
930
+ file: Path | None = typer.Option(
931
+ None, "--file", help="JSON file with fields to update."
932
+ ),
933
+ name: str | None = typer.Option(None, "--name"),
934
+ prompt: str | None = typer.Option(None, "--prompt"),
935
+ modality: str | None = typer.Option(None),
936
+ callback_url: str | None = typer.Option(None),
937
+ tool_call: str | None = typer.Option(None, help="JSON object."),
938
+ app_message: bool | None = typer.Option(None, "--app-message/--no-app-message"),
939
+ tags: list[str] | None = typer.Option(None, "--tag"),
940
+ ) -> None:
941
+ """Patch a guardrail. RQH replaces each supplied field whole."""
942
+
943
+ async def run() -> Any:
944
+ if file:
945
+ body = json.loads(file.read_text())
946
+ else:
947
+ body = {}
948
+ if name is not None:
949
+ body["guardrail_name"] = name
950
+ if prompt is not None:
951
+ body["guardrail_prompt"] = prompt
952
+ if modality is not None:
953
+ body["modality"] = modality
954
+ if callback_url is not None:
955
+ body["callback_url"] = callback_url
956
+ if tool_call is not None:
957
+ body["tool_call"] = json.loads(tool_call)
958
+ if app_message is not None:
959
+ body["app_message"] = app_message
960
+ if tags is not None:
961
+ body["tags"] = list(tags)
962
+ async with TavusClient.from_env() as client:
963
+ return await client.guardrails.patch(guardrail_id, body)
964
+
965
+ _json(_run(run()))
966
+
967
+
968
+ @guardrail_app.command("delete")
969
+ def guardrail_delete(guardrail_id: str) -> None:
970
+ async def run() -> Any:
971
+ async with TavusClient.from_env() as client:
972
+ return await client.guardrails.delete(guardrail_id)
973
+
974
+ _json(_run(run()))
975
+
976
+
977
+ @guardrail_app.command("tags")
978
+ def guardrail_tags(
979
+ search: str | None = typer.Option(None),
980
+ page: int | None = typer.Option(None),
981
+ limit: int | None = typer.Option(None),
982
+ ) -> None:
983
+ """List tags applied to the account's guardrails."""
984
+
985
+ async def run() -> Any:
986
+ async with TavusClient.from_env() as client:
987
+ return await client.guardrails.tags(
988
+ search=search, page=page, limit=limit
989
+ )
990
+
991
+ _json(_run(run()))
992
+
993
+
994
+ @objective_app.command("list")
995
+ def objective_list(
996
+ limit: int = typer.Option(25),
997
+ page: int = typer.Option(1),
998
+ type: str = typer.Option("user", help="user | system | all"),
999
+ name_or_uuid: str | None = typer.Option(None),
1000
+ sort: str = typer.Option("ascending"),
1001
+ json_output: bool = typer.Option(False, "--json"),
1002
+ ) -> None:
1003
+ """List objective sets."""
1004
+
1005
+ async def run() -> Any:
1006
+ params: dict[str, Any] = {
1007
+ "limit": limit,
1008
+ "page": page,
1009
+ "type": type,
1010
+ "sort": sort,
1011
+ }
1012
+ if name_or_uuid:
1013
+ params["name_or_uuid"] = name_or_uuid
1014
+ async with TavusClient.from_env() as client:
1015
+ return await client.objectives.list(**params)
1016
+
1017
+ data = _run(run())
1018
+ if json_output:
1019
+ _json(data)
1020
+ return
1021
+ _print_rows(
1022
+ data, ["objectives_id", "objectives_name", "updated_at"]
1023
+ )
1024
+
1025
+
1026
+ @objective_app.command("get")
1027
+ def objective_get(objectives_id: str) -> None:
1028
+ async def run() -> Any:
1029
+ async with TavusClient.from_env() as client:
1030
+ return await client.objectives.get(objectives_id)
1031
+
1032
+ _json(_run(run()))
1033
+
1034
+
1035
+ @objective_app.command("create")
1036
+ def objective_create(
1037
+ file: Path = typer.Option(
1038
+ ...,
1039
+ "--file",
1040
+ help="JSON file with {name, data, allow_loops}.",
1041
+ ),
1042
+ ) -> None:
1043
+ """Create an objective set from a JSON file."""
1044
+
1045
+ async def run() -> Any:
1046
+ body = json.loads(file.read_text())
1047
+ async with TavusClient.from_env() as client:
1048
+ return await client.objectives.create(body)
1049
+
1050
+ _json(_run(run()))
1051
+
1052
+
1053
+ @objective_app.command("patch")
1054
+ def objective_patch(
1055
+ objectives_id: str,
1056
+ file: Path = typer.Option(
1057
+ ..., "--file", help="JSON file with a JSON Patch ops array."
1058
+ ),
1059
+ ) -> None:
1060
+ """Patch an objective set with JSON Patch ops."""
1061
+
1062
+ async def run() -> Any:
1063
+ ops = json.loads(file.read_text())
1064
+ async with TavusClient.from_env() as client:
1065
+ return await client.objectives.patch(objectives_id, ops)
1066
+
1067
+ _json(_run(run()))
1068
+
1069
+
1070
+ @objective_app.command("delete")
1071
+ def objective_delete(objectives_id: str) -> None:
1072
+ async def run() -> Any:
1073
+ async with TavusClient.from_env() as client:
1074
+ return await client.objectives.delete(objectives_id)
1075
+
1076
+ _json(_run(run()))
1077
+
1078
+
1079
+ @objective_app.command("validate")
1080
+ def objective_validate(
1081
+ file: Path = typer.Option(
1082
+ ..., "--file", help="JSON file with {name, data, allow_loops}."
1083
+ ),
1084
+ ) -> None:
1085
+ """Validate an objective-set payload without persisting."""
1086
+
1087
+ async def run() -> Any:
1088
+ body = json.loads(file.read_text())
1089
+ async with TavusClient.from_env() as client:
1090
+ return await client.objectives.validate(body)
1091
+
1092
+ _json(_run(run()))
1093
+
1094
+
1095
+ @objective_app.command("example")
1096
+ def objective_example() -> None:
1097
+ """Print a starter JSON body for ``tavus objective create --file``."""
1098
+ _json(
1099
+ {
1100
+ "name": "support_intake",
1101
+ "allow_loops": False,
1102
+ "data": [
1103
+ {
1104
+ "objective_name": "greet",
1105
+ "objective_prompt": "Greet the caller and ask for their name.",
1106
+ "output_variables": ["caller_name"],
1107
+ "next_required_objective": "diagnose",
1108
+ },
1109
+ {
1110
+ "objective_name": "diagnose",
1111
+ "objective_prompt": "Ask what problem they're calling about.",
1112
+ "next_conditional_objectives": {
1113
+ "billing": "the caller mentions billing, charges, refund",
1114
+ "technical": "the caller mentions an error, outage, broken feature",
1115
+ },
1116
+ },
1117
+ {
1118
+ "objective_name": "billing",
1119
+ "objective_prompt": "Hand off to billing support.",
1120
+ },
1121
+ {
1122
+ "objective_name": "technical",
1123
+ "objective_prompt": "Collect error details and offer a callback.",
1124
+ },
1125
+ ],
1126
+ }
1127
+ )
1128
+
1129
+
1130
+ @tool_app.command("list")
1131
+ def tool_list(
1132
+ limit: int = typer.Option(25),
1133
+ page: int = typer.Option(1),
1134
+ type: str = typer.Option("user", help="user | system | all"),
1135
+ name_or_uuid: str | None = typer.Option(None),
1136
+ sort: str = typer.Option("ascending"),
1137
+ json_output: bool = typer.Option(False, "--json"),
1138
+ ) -> None:
1139
+ """List tools."""
1140
+
1141
+ async def run() -> Any:
1142
+ params: dict[str, Any] = {
1143
+ "limit": limit,
1144
+ "page": page,
1145
+ "type": type,
1146
+ "sort": sort,
1147
+ }
1148
+ if name_or_uuid:
1149
+ params["name_or_uuid"] = name_or_uuid
1150
+ async with TavusClient.from_env() as client:
1151
+ return await client.tools.list(**params)
1152
+
1153
+ data = _run(run())
1154
+ if json_output:
1155
+ _json(data)
1156
+ return
1157
+ _print_rows(data, ["tool_id", "name", "origin", "on_call", "updated_at"])
1158
+
1159
+
1160
+ @tool_app.command("get")
1161
+ def tool_get(tool_id: str) -> None:
1162
+ async def run() -> Any:
1163
+ async with TavusClient.from_env() as client:
1164
+ return await client.tools.get(tool_id)
1165
+
1166
+ _json(_run(run()))
1167
+
1168
+
1169
+ @tool_app.command("create")
1170
+ def tool_create(
1171
+ file: Path = typer.Option(
1172
+ ...,
1173
+ "--file",
1174
+ help="JSON file with the full tool body (name/description/parameters/delivery/...).",
1175
+ ),
1176
+ ) -> None:
1177
+ """Create a tool from a JSON file. The file mirrors the `POST /v2/tools`
1178
+ body shape — see ``ToolCreate`` in the SDK for fields, or
1179
+ ``tavus tool example`` for a starter."""
1180
+
1181
+ async def run() -> Any:
1182
+ body = json.loads(file.read_text())
1183
+ async with TavusClient.from_env() as client:
1184
+ return await client.tools.create(body)
1185
+
1186
+ _json(_run(run()))
1187
+
1188
+
1189
+ @tool_app.command("patch")
1190
+ def tool_patch(
1191
+ tool_id: str,
1192
+ file: Path = typer.Option(
1193
+ ..., "--file", help="JSON file with fields to update."
1194
+ ),
1195
+ ) -> None:
1196
+ """Patch a tool from a JSON file. RQH rejects PATCHes that echo back
1197
+ the scrubbed-secret placeholder (``********``) from a prior GET — omit
1198
+ secret fields you don't intend to change."""
1199
+
1200
+ async def run() -> Any:
1201
+ body = json.loads(file.read_text())
1202
+ async with TavusClient.from_env() as client:
1203
+ return await client.tools.patch(tool_id, body)
1204
+
1205
+ _json(_run(run()))
1206
+
1207
+
1208
+ @tool_app.command("delete")
1209
+ def tool_delete(tool_id: str) -> None:
1210
+ async def run() -> Any:
1211
+ async with TavusClient.from_env() as client:
1212
+ return await client.tools.delete(tool_id)
1213
+
1214
+ _json(_run(run()))
1215
+
1216
+
1217
+ @tool_app.command("example")
1218
+ def tool_example(
1219
+ delivery: str = typer.Option(
1220
+ "app_message", help="app_message | http | http_oauth2"
1221
+ ),
1222
+ ) -> None:
1223
+ """Print a starter JSON body for ``tavus tool create --file``. Edit the
1224
+ output and save to a file, then pass it to ``create``."""
1225
+ examples = {
1226
+ "app_message": {
1227
+ "name": "weather_lookup",
1228
+ "description": "Get the current weather for a city.",
1229
+ "parameters": {
1230
+ "type": "object",
1231
+ "properties": {
1232
+ "city": {"type": "string", "description": "City name."}
1233
+ },
1234
+ "required": ["city"],
1235
+ },
1236
+ "delivery": {"app_message": True},
1237
+ "origin": "llm",
1238
+ "on_call": "generate_filler",
1239
+ "on_resolve": "generate_response",
1240
+ },
1241
+ "http": {
1242
+ "name": "weather_lookup",
1243
+ "description": "Look up current weather via a third-party HTTPS endpoint.",
1244
+ "parameters": {
1245
+ "type": "object",
1246
+ "properties": {
1247
+ "city": {"type": "string", "description": "City name."}
1248
+ },
1249
+ "required": ["city"],
1250
+ },
1251
+ "delivery": {
1252
+ "api": {
1253
+ "url": "https://api.example.com/weather/{city}",
1254
+ "method": "GET",
1255
+ "timeout": 10,
1256
+ "headers": {"Accept": "application/json"},
1257
+ "auth": {"type": "bearer", "token": "sk-..."},
1258
+ }
1259
+ },
1260
+ "origin": "llm",
1261
+ "on_call": "generate_filler",
1262
+ "on_resolve": "generate_response",
1263
+ },
1264
+ "http_oauth2": {
1265
+ "name": "salesforce_lead_create",
1266
+ "description": "Create a Salesforce lead via OAuth2 client credentials.",
1267
+ "parameters": {
1268
+ "type": "object",
1269
+ "properties": {
1270
+ "email": {"type": "string"},
1271
+ "name": {"type": "string"},
1272
+ },
1273
+ "required": ["email", "name"],
1274
+ },
1275
+ "delivery": {
1276
+ "api": {
1277
+ "url": "https://acme.my.salesforce.com/services/data/v59.0/sobjects/Lead",
1278
+ "method": "POST",
1279
+ "body_template": {"Email": "{email}", "LastName": "{name}"},
1280
+ "auth": {
1281
+ "type": "oauth2_client_credentials",
1282
+ "token_url": "https://acme.my.salesforce.com/services/oauth2/token",
1283
+ "client_id": "...",
1284
+ "client_secret": "...",
1285
+ },
1286
+ }
1287
+ },
1288
+ "origin": "llm",
1289
+ "on_call": "generate_filler",
1290
+ "on_resolve": "fire_and_forget",
1291
+ },
1292
+ }
1293
+ if delivery not in examples:
1294
+ raise typer.BadParameter(
1295
+ f"delivery must be one of {sorted(examples)}", param_hint="--delivery"
1296
+ )
1297
+ _json(examples[delivery])
1298
+
1299
+
1300
+ @pronunciation_app.command("list")
1301
+ def pronunciation_list(
1302
+ limit: int = typer.Option(25),
1303
+ page: int = typer.Option(0),
1304
+ sort: str = typer.Option("desc"),
1305
+ json_output: bool = typer.Option(False, "--json"),
1306
+ ) -> None:
1307
+ """List pronunciation dictionaries — referenced from
1308
+ ``layers.tts.pronunciation_dictionary_id``."""
1309
+
1310
+ async def run() -> Any:
1311
+ async with TavusClient.from_env() as client:
1312
+ return await client.pronunciation_dictionaries.list(
1313
+ limit=limit, page=page, sort=sort
1314
+ )
1315
+
1316
+ data = _run(run())
1317
+ if json_output:
1318
+ _json(data)
1319
+ return
1320
+ _print_rows(data, ["uuid", "name", "updated_at"])
1321
+
1322
+
1323
+ @pronunciation_app.command("get")
1324
+ def pronunciation_get(dictionary_id: str) -> None:
1325
+ async def run() -> Any:
1326
+ async with TavusClient.from_env() as client:
1327
+ return await client.pronunciation_dictionaries.get(dictionary_id)
1328
+
1329
+ _json(_run(run()))
1330
+
1331
+
1332
+ @pronunciation_app.command("create")
1333
+ def pronunciation_create(
1334
+ file: Path = typer.Option(
1335
+ ..., "--file", help="JSON file with {name, rules}."
1336
+ ),
1337
+ ) -> None:
1338
+ """Create a pronunciation dictionary from a JSON file. Rules are
1339
+ ``{text, pronunciation, type, alphabet?, case_sensitive?, word_boundaries?}``."""
1340
+
1341
+ async def run() -> Any:
1342
+ body = json.loads(file.read_text())
1343
+ async with TavusClient.from_env() as client:
1344
+ return await client.pronunciation_dictionaries.create(body)
1345
+
1346
+ _json(_run(run()))
1347
+
1348
+
1349
+ @pronunciation_app.command("patch")
1350
+ def pronunciation_patch(
1351
+ dictionary_id: str,
1352
+ file: Path = typer.Option(
1353
+ ..., "--file", help="JSON file with fields to update."
1354
+ ),
1355
+ ) -> None:
1356
+ """Patch a pronunciation dictionary. Supplying ``rules`` replaces the
1357
+ full list (RQH does not merge rule arrays)."""
1358
+
1359
+ async def run() -> Any:
1360
+ body = json.loads(file.read_text())
1361
+ async with TavusClient.from_env() as client:
1362
+ return await client.pronunciation_dictionaries.patch(
1363
+ dictionary_id, body
1364
+ )
1365
+
1366
+ _json(_run(run()))
1367
+
1368
+
1369
+ @pronunciation_app.command("delete")
1370
+ def pronunciation_delete(dictionary_id: str) -> None:
1371
+ async def run() -> Any:
1372
+ async with TavusClient.from_env() as client:
1373
+ return await client.pronunciation_dictionaries.delete(dictionary_id)
1374
+
1375
+ _json(_run(run()))
1376
+
1377
+
1378
+ @pronunciation_app.command("example")
1379
+ def pronunciation_example() -> None:
1380
+ """Print a starter JSON body for ``pronunciation-dictionary create``."""
1381
+ _json(
1382
+ {
1383
+ "name": "tavus-defaults",
1384
+ "rules": [
1385
+ {
1386
+ "text": "tavus",
1387
+ "pronunciation": "TAH-vus",
1388
+ "type": "alias",
1389
+ },
1390
+ {
1391
+ "text": "kubernetes",
1392
+ "pronunciation": "ˌkuːbərˈnɛtɪz",
1393
+ "type": "ipa",
1394
+ },
1395
+ ],
1396
+ }
1397
+ )
1398
+
1399
+
1400
+ @persona_tools_app.command("list")
1401
+ def persona_tools_list(persona_id: str) -> None:
1402
+ """List tools attached to a persona."""
1403
+
1404
+ async def run() -> Any:
1405
+ async with TavusClient.from_env() as client:
1406
+ return await client.personas.list_tools(persona_id)
1407
+
1408
+ _json(_run(run()))
1409
+
1410
+
1411
+ @persona_tools_app.command("attach")
1412
+ def persona_tools_attach(
1413
+ persona_id: str,
1414
+ tool_ids: list[str] = typer.Argument(..., help="One or more tool_id values."),
1415
+ ) -> None:
1416
+ """Attach existing tools to a persona by tool_id."""
1417
+
1418
+ async def run() -> Any:
1419
+ async with TavusClient.from_env() as client:
1420
+ return await client.personas.attach_tools(persona_id, list(tool_ids))
1421
+
1422
+ _json(_run(run()))
1423
+
1424
+
1425
+ @persona_tools_app.command("detach")
1426
+ def persona_tools_detach(persona_id: str, tool_id: str) -> None:
1427
+ """Detach a tool from a persona."""
1428
+
1429
+ async def run() -> Any:
1430
+ async with TavusClient.from_env() as client:
1431
+ return await client.personas.detach_tool(persona_id, tool_id)
1432
+
1433
+ _json(_run(run()))
1434
+
1435
+
1436
+ def _resource(client: TavusClient, resource: str) -> Any:
1437
+ normalized = resource.strip().lower().replace("-", "_")
1438
+ aliases = {
1439
+ "guardrail": "guardrails",
1440
+ "objective": "objectives",
1441
+ "document": "documents",
1442
+ "voice": "voices",
1443
+ "tool": "tools",
1444
+ }
1445
+ attr = aliases.get(normalized, normalized)
1446
+ if attr not in {"guardrails", "objectives", "documents", "voices", "tools"}:
1447
+ raise typer.BadParameter(
1448
+ "resource must be guardrails, objectives, documents, voices, or tools"
1449
+ )
1450
+ return getattr(client, attr)
1451
+
1452
+
1453
+ def _print_rows(data: Any, columns: list[str]) -> None:
1454
+ rows = data.get("data", data) if isinstance(data, dict) else data
1455
+ if not isinstance(rows, list):
1456
+ _json(data)
1457
+ return
1458
+ table = Table()
1459
+ for column in columns:
1460
+ table.add_column(column)
1461
+ for row in rows:
1462
+ table.add_row(*[str(row.get(column, "")) for column in columns])
1463
+ console.print(table)
1464
+
1465
+
1466
+ if __name__ == "__main__":
1467
+ app()