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_cli-0.1.0.dist-info/METADATA +192 -0
- tavus_cli-0.1.0.dist-info/RECORD +32 -0
- tavus_cli-0.1.0.dist-info/WHEEL +4 -0
- tavus_cli-0.1.0.dist-info/entry_points.txt +3 -0
- tavus_mcp/__init__.py +4 -0
- tavus_mcp/cli/__init__.py +1 -0
- tavus_mcp/cli/main.py +1467 -0
- tavus_mcp/sdk/__init__.py +6 -0
- tavus_mcp/sdk/auth/__init__.py +1 -0
- tavus_mcp/sdk/auth/keyring_store.py +20 -0
- tavus_mcp/sdk/auth/oauth.py +131 -0
- tavus_mcp/sdk/auth/session.py +46 -0
- tavus_mcp/sdk/client/__init__.py +3 -0
- tavus_mcp/sdk/client/http.py +451 -0
- tavus_mcp/sdk/env.py +126 -0
- tavus_mcp/sdk/errors.py +46 -0
- tavus_mcp/sdk/patch.py +92 -0
- tavus_mcp/sdk/recipes/__init__.py +6 -0
- tavus_mcp/sdk/recipes/build_and_verify.py +618 -0
- tavus_mcp/sdk/recipes/options.py +67 -0
- tavus_mcp/sdk/recipes/quickstart.py +48 -0
- tavus_mcp/sdk/recipes/scaffold_embed.py +115 -0
- tavus_mcp/sdk/recipes/templates.py +58 -0
- tavus_mcp/sdk/recipes/tool_reuse.py +124 -0
- tavus_mcp/sdk/schemas/__init__.py +16 -0
- tavus_mcp/sdk/schemas/file_manifest.py +21 -0
- tavus_mcp/sdk/schemas/guardrail.py +84 -0
- tavus_mcp/sdk/schemas/objective.py +131 -0
- tavus_mcp/sdk/schemas/persona.py +174 -0
- tavus_mcp/sdk/schemas/pronunciation.py +73 -0
- tavus_mcp/sdk/schemas/tool.py +349 -0
- tavus_mcp/server.py +877 -0
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
|
+
)
|
tavus_mcp/sdk/errors.py
ADDED
|
@@ -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"]
|