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/sdk/env.py ADDED
@@ -0,0 +1,126 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ from enum import StrEnum
5
+ from pathlib import Path
6
+
7
+ from dotenv import load_dotenv
8
+ from pydantic import BaseModel, ConfigDict, Field, HttpUrl, field_validator
9
+
10
+ from tavus_mcp.sdk.errors import TavusConfigError
11
+
12
+
13
+ class TavusEnvironment(StrEnum):
14
+ TEST = "TEST"
15
+ STG = "STG"
16
+ PROD = "PROD"
17
+
18
+
19
+ DEFAULT_ENV = TavusEnvironment.PROD
20
+
21
+
22
+ ENV_ALIASES = {
23
+ "TEST": TavusEnvironment.TEST,
24
+ "TEST_DB": TavusEnvironment.TEST,
25
+ "DEV": TavusEnvironment.TEST,
26
+ "LOCAL": TavusEnvironment.TEST,
27
+ "STG": TavusEnvironment.STG,
28
+ "STAGE": TavusEnvironment.STG,
29
+ "STAGING": TavusEnvironment.STG,
30
+ "PROD": TavusEnvironment.PROD,
31
+ "PRODUCTION": TavusEnvironment.PROD,
32
+ }
33
+
34
+
35
+ DEFAULT_PUBLIC_API_BASE_URLS = {
36
+ TavusEnvironment.TEST: "https://test.rqh.tavusapi.com/v2",
37
+ TavusEnvironment.STG: "https://stg.rqh.tavusapi.com/v2",
38
+ TavusEnvironment.PROD: "https://tavusapi.com/v2",
39
+ }
40
+
41
+ DEFAULT_PORTAL_API_BASE_URLS = {
42
+ TavusEnvironment.TEST: "https://test-api.tavus.io/api",
43
+ TavusEnvironment.STG: "https://stg-api.tavus.io/api",
44
+ TavusEnvironment.PROD: "https://prod-api.tavus.io/api",
45
+ }
46
+
47
+ DEFAULT_DEV_PORTAL_URLS = {
48
+ TavusEnvironment.TEST: "https://dev.platform.tavus.io",
49
+ TavusEnvironment.STG: "https://stage.platform.tavus.io",
50
+ TavusEnvironment.PROD: "https://platform.tavus.io",
51
+ }
52
+
53
+
54
+ class TavusConfig(BaseModel):
55
+ model_config = ConfigDict(frozen=True)
56
+
57
+ env: TavusEnvironment = DEFAULT_ENV
58
+ public_api_base_url: HttpUrl
59
+ portal_api_base_url: HttpUrl
60
+ dev_portal_url: HttpUrl
61
+ keyring_service: str = "tavus_mcp"
62
+ env_file: Path | None = Field(default=None)
63
+
64
+ @field_validator("public_api_base_url", "portal_api_base_url", "dev_portal_url")
65
+ @classmethod
66
+ def strip_trailing_slash(cls, value: HttpUrl) -> HttpUrl:
67
+ return HttpUrl(str(value).rstrip("/"))
68
+
69
+ @property
70
+ def keyring_username(self) -> str:
71
+ return self.env.value.lower()
72
+
73
+
74
+ def _load_dotenv(cwd: Path | None) -> Path | None:
75
+ env_file = (cwd or Path.cwd()) / ".env"
76
+ if env_file.exists():
77
+ load_dotenv(env_file, override=False)
78
+ return env_file
79
+ return None
80
+
81
+
82
+ def _env_name() -> TavusEnvironment:
83
+ raw = os.getenv("TAVUS_ENV", DEFAULT_ENV.value).strip().upper()
84
+ try:
85
+ return ENV_ALIASES[raw]
86
+ except KeyError as exc:
87
+ allowed = ", ".join(sorted(ENV_ALIASES))
88
+ raise TavusConfigError(
89
+ f"Unsupported TAVUS_ENV={raw!r}. Expected one of: {allowed}"
90
+ ) from exc
91
+
92
+
93
+ def _configured_url(kind: str, env: TavusEnvironment) -> str:
94
+ generic = os.getenv(f"TAVUS_{kind}_API_BASE_URL")
95
+ if generic:
96
+ return generic
97
+
98
+ env_specific = os.getenv(f"TAVUS_{env.value}_{kind}_API_BASE_URL")
99
+ if env_specific:
100
+ return env_specific
101
+
102
+ defaults = DEFAULT_PUBLIC_API_BASE_URLS if kind == "PUBLIC" else DEFAULT_PORTAL_API_BASE_URLS
103
+ return defaults[env]
104
+
105
+
106
+ def _configured_dev_portal_url(env: TavusEnvironment) -> str:
107
+ generic = os.getenv("TAVUS_DEV_PORTAL_URL")
108
+ if generic:
109
+ return generic
110
+ env_specific = os.getenv(f"TAVUS_{env.value}_DEV_PORTAL_URL")
111
+ if env_specific:
112
+ return env_specific
113
+ return DEFAULT_DEV_PORTAL_URLS[env]
114
+
115
+
116
+ def load_config(*, cwd: Path | None = None) -> TavusConfig:
117
+ env_file = _load_dotenv(cwd)
118
+ env = _env_name()
119
+ return TavusConfig(
120
+ env=env,
121
+ public_api_base_url=_configured_url("PUBLIC", env),
122
+ portal_api_base_url=_configured_url("PORTAL", env),
123
+ dev_portal_url=_configured_dev_portal_url(env),
124
+ keyring_service=os.getenv("TAVUS_KEYRING_SERVICE", "tavus_mcp"),
125
+ env_file=env_file,
126
+ )
@@ -0,0 +1,46 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+
6
+ class TavusError(Exception):
7
+ """Base package error."""
8
+
9
+
10
+ class TavusConfigError(TavusError):
11
+ """Invalid local configuration."""
12
+
13
+
14
+ class TavusAuthError(TavusError):
15
+ """Missing or invalid auth credentials."""
16
+
17
+
18
+ class TavusApiError(TavusError):
19
+ """HTTP API failure with structured context."""
20
+
21
+ def __init__(self, message: str, *, status_code: int | None = None, body: Any = None) -> None:
22
+ super().__init__(message)
23
+ self.status_code = status_code
24
+ self.body = body
25
+
26
+
27
+ class TavusPatchValidationError(TavusError):
28
+ """Invalid JSON Patch operation before it was sent to Tavus."""
29
+
30
+ def __init__(
31
+ self,
32
+ message: str,
33
+ *,
34
+ path: str | None = None,
35
+ suggestions: list[str] | None = None,
36
+ ) -> None:
37
+ super().__init__(message)
38
+ self.path = path
39
+ self.suggestions = suggestions or []
40
+
41
+ def to_dict(self) -> dict[str, Any]:
42
+ return {
43
+ "error": str(self),
44
+ "path": self.path,
45
+ "suggestions": self.suggestions,
46
+ }
tavus_mcp/sdk/patch.py ADDED
@@ -0,0 +1,92 @@
1
+ from __future__ import annotations
2
+
3
+ import difflib
4
+ from typing import Any, get_args, get_origin
5
+
6
+ from pydantic import BaseModel
7
+
8
+ from tavus_mcp.sdk.errors import TavusPatchValidationError
9
+ from tavus_mcp.sdk.schemas.persona import JSONPatchOperation, PersonaPatchModel
10
+
11
+
12
+ def allowed_patch_paths(model: type[BaseModel] = PersonaPatchModel) -> set[str]:
13
+ paths: set[str] = set()
14
+ _collect_model_paths(model, "", paths)
15
+ return paths
16
+
17
+
18
+ def validate_patch_operations(ops: list[dict[str, Any]]) -> list[dict[str, Any]]:
19
+ allowed = allowed_patch_paths()
20
+ validated: list[dict[str, Any]] = []
21
+
22
+ for raw in ops:
23
+ op = JSONPatchOperation.model_validate(raw).model_dump(by_alias=True, exclude_none=True)
24
+ path = str(op["path"])
25
+ _validate_path(path, allowed)
26
+ if op["op"] in {"move", "copy"} and "from" in op:
27
+ _validate_path(str(op["from"]), allowed)
28
+ validated.append(op)
29
+ return validated
30
+
31
+
32
+ def _validate_path(path: str, allowed: set[str]) -> None:
33
+ if not path.startswith("/"):
34
+ raise TavusPatchValidationError("JSON Patch path must start with '/'.", path=path)
35
+ if path in allowed:
36
+ return
37
+
38
+ parts = [part for part in path.split("/") if part]
39
+ prefixes = ["/" + "/".join(parts[:i]) for i in range(1, len(parts) + 1)]
40
+ if any(prefix in allowed and _is_dynamic_child_allowed(prefix) for prefix in prefixes):
41
+ return
42
+
43
+ suggestions = difflib.get_close_matches(path, sorted(allowed), n=5)
44
+ raise TavusPatchValidationError(
45
+ f"Unsupported persona patch path: {path}",
46
+ path=path,
47
+ suggestions=suggestions,
48
+ )
49
+
50
+
51
+ def _is_dynamic_child_allowed(prefix: str) -> bool:
52
+ return prefix.endswith(
53
+ (
54
+ "/tools",
55
+ "/headers",
56
+ "/extra_body",
57
+ "/default_query",
58
+ "/voice_settings",
59
+ "/visual_tools",
60
+ "/audio_tools",
61
+ "/perception_tools",
62
+ "/transport",
63
+ "/sts",
64
+ )
65
+ )
66
+
67
+
68
+ def _collect_model_paths(model: type[BaseModel], base: str, paths: set[str]) -> None:
69
+ for name, field in model.model_fields.items():
70
+ path = f"{base}/{name}"
71
+ paths.add(path)
72
+ annotation = _unwrap_optional(field.annotation)
73
+ origin = get_origin(annotation)
74
+
75
+ if isinstance(annotation, type) and issubclass(annotation, BaseModel):
76
+ _collect_model_paths(annotation, path, paths)
77
+ elif origin is list:
78
+ paths.add(f"{path}/-")
79
+ paths.add(f"{path}/0")
80
+ elif origin is dict:
81
+ paths.add(f"{path}/*")
82
+
83
+
84
+ def _unwrap_optional(annotation: Any) -> Any:
85
+ origin = get_origin(annotation)
86
+ if origin in {type(None), None}:
87
+ return annotation
88
+ if origin in {__import__("types").UnionType, getattr(__import__("typing"), "Union", object)}:
89
+ args = [arg for arg in get_args(annotation) if arg is not type(None)]
90
+ if len(args) == 1:
91
+ return args[0]
92
+ return annotation
@@ -0,0 +1,6 @@
1
+ from tavus_mcp.sdk.recipes.options import describe_persona_options
2
+ from tavus_mcp.sdk.recipes.quickstart import quickstart
3
+ from tavus_mcp.sdk.recipes.scaffold_embed import scaffold_embed
4
+ from tavus_mcp.sdk.recipes.templates import persona_from_template
5
+
6
+ __all__ = ["describe_persona_options", "persona_from_template", "quickstart", "scaffold_embed"]