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.
@@ -0,0 +1,48 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ from tavus_mcp.sdk.client.http import TavusClient
6
+
7
+
8
+ async def quickstart(
9
+ client: TavusClient,
10
+ *,
11
+ system_prompt: str,
12
+ persona_name: str = "Agentic Tavus Persona",
13
+ replica_id: str | None = None,
14
+ conversation_name: str | None = None,
15
+ ) -> dict[str, Any]:
16
+ selected_replica_id = replica_id or await _first_stock_replica_id(client)
17
+ persona = await client.personas.create(
18
+ {
19
+ "persona_name": persona_name,
20
+ "system_prompt": system_prompt,
21
+ "default_replica_id": selected_replica_id,
22
+ }
23
+ )
24
+ persona_id = persona["persona_id"]
25
+ conversation = await client.conversations.create(
26
+ {
27
+ "persona_id": persona_id,
28
+ "replica_id": selected_replica_id,
29
+ "conversation_name": conversation_name or persona_name,
30
+ }
31
+ )
32
+ return {
33
+ "persona": persona,
34
+ "conversation": conversation,
35
+ "replica_id": selected_replica_id,
36
+ }
37
+
38
+
39
+ async def _first_stock_replica_id(client: TavusClient) -> str:
40
+ replicas = await client.replicas.list(limit=25, replica_type="system")
41
+ data = replicas.get("data", replicas) if isinstance(replicas, dict) else replicas
42
+ if not data:
43
+ raise ValueError("No stock replicas were returned by the selected Tavus environment.")
44
+ first = data[0]
45
+ replica_id = first.get("replica_id") or first.get("replica_uuid") or first.get("id")
46
+ if not replica_id:
47
+ raise ValueError("Stock replica response did not include a replica id.")
48
+ return str(replica_id)
@@ -0,0 +1,115 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Literal
4
+
5
+ from tavus_mcp.sdk.schemas.file_manifest import FileEntry, FileManifest
6
+
7
+ EmbedTarget = Literal["iframe", "cvi-ui", "vanilla"]
8
+
9
+
10
+ IFRAME_ALLOW = "camera; microphone; fullscreen; display-capture; autoplay"
11
+
12
+
13
+ def scaffold_embed(
14
+ *,
15
+ conversation_url: str,
16
+ target: EmbedTarget = "iframe",
17
+ component_name: str = "TavusConversation",
18
+ ) -> FileManifest:
19
+ if target == "iframe":
20
+ return _iframe(conversation_url)
21
+ if target == "cvi-ui":
22
+ return _cvi_ui(conversation_url, component_name)
23
+ if target == "vanilla":
24
+ return _vanilla(conversation_url)
25
+ raise ValueError(f"Unsupported embed target: {target}")
26
+
27
+
28
+ def _iframe(conversation_url: str) -> FileManifest:
29
+ return FileManifest(
30
+ files=[
31
+ FileEntry(
32
+ path="tavus-embed.html",
33
+ description="Standalone Tavus iframe embed.",
34
+ content=f"""<iframe
35
+ src="{conversation_url}"
36
+ allow="{IFRAME_ALLOW}"
37
+ allowfullscreen
38
+ style="width: 100%; height: 720px; border: 0;"
39
+ ></iframe>
40
+ """,
41
+ )
42
+ ],
43
+ notes=["The iframe allow attribute includes display-capture for screen share support."],
44
+ )
45
+
46
+
47
+ def _cvi_ui(conversation_url: str, component_name: str) -> FileManifest:
48
+ return FileManifest(
49
+ files=[
50
+ FileEntry(
51
+ path=f"src/components/{component_name}.tsx",
52
+ description=(
53
+ "@tavus/cvi-ui React component. Install and initialize "
54
+ "@tavus/cvi-ui first."
55
+ ),
56
+ content=f"""import {{ useCallback }} from 'react';
57
+
58
+ const CONVERSATION_URL = {conversation_url!r};
59
+
60
+ export function {component_name}() {{
61
+ const start = useCallback(() => {{
62
+ window.open(CONVERSATION_URL, '_blank', 'noopener,noreferrer');
63
+ }}, []);
64
+
65
+ return (
66
+ <button type="button" onClick={{start}}>
67
+ Start Tavus Conversation
68
+ </button>
69
+ );
70
+ }}
71
+ """,
72
+ )
73
+ ],
74
+ notes=[
75
+ "This starter preserves the mobile Safari user-gesture chain by joining from a click.",
76
+ (
77
+ "For a full in-app experience, run `npx @tavus/cvi-ui@latest init` "
78
+ "and wire the copied components."
79
+ ),
80
+ ],
81
+ )
82
+
83
+
84
+ def _vanilla(conversation_url: str) -> FileManifest:
85
+ return FileManifest(
86
+ files=[
87
+ FileEntry(
88
+ path="tavus-vanilla.html",
89
+ description="Vanilla Daily iframe embed.",
90
+ content=f"""<!doctype html>
91
+ <html lang="en">
92
+ <head>
93
+ <meta charset="utf-8" />
94
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
95
+ <title>Tavus Conversation</title>
96
+ <script crossorigin src="https://unpkg.com/@daily-co/daily-js"></script>
97
+ </head>
98
+ <body>
99
+ <button id="start">Start</button>
100
+ <div id="call" style="width: 100%; height: 720px;"></div>
101
+ <script>
102
+ const frame = window.DailyIframe.createFrame(document.getElementById('call'), {{
103
+ iframeStyle: {{ width: '100%', height: '100%', border: '0' }}
104
+ }});
105
+ document.getElementById('start').addEventListener('click', () => {{
106
+ frame.join({{ url: {conversation_url!r} }});
107
+ }});
108
+ </script>
109
+ </body>
110
+ </html>
111
+ """,
112
+ )
113
+ ],
114
+ notes=["Join is bound to a click to preserve browser media permissions."],
115
+ )
@@ -0,0 +1,58 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Literal
4
+
5
+ from tavus_mcp.sdk.client.http import TavusClient
6
+
7
+ PersonaTemplate = Literal["customer-support", "interviewer", "sales", "tutor", "dev-rel"]
8
+
9
+ TEMPLATES: dict[str, str] = {
10
+ "customer-support": (
11
+ "You are a warm, concise customer support specialist. Resolve the user's issue, "
12
+ "ask focused follow-up questions when details are missing, and avoid overpromising."
13
+ ),
14
+ "interviewer": (
15
+ "You are a structured interviewer. Ask one question at a time, listen carefully, "
16
+ "probe for concrete examples, and keep the conversation professional."
17
+ ),
18
+ "sales": (
19
+ "You are a consultative sales assistant. Discover the user's needs, qualify fit, "
20
+ "explain value in practical terms, and never pressure the user."
21
+ ),
22
+ "tutor": (
23
+ "You are a patient tutor. Explain concepts step by step, check understanding, "
24
+ "adapt to the learner's pace, and use examples before abstractions."
25
+ ),
26
+ "dev-rel": (
27
+ "You are a developer relations guide. Help developers build successfully, give "
28
+ "precise technical answers, and include implementation tradeoffs when relevant."
29
+ ),
30
+ }
31
+
32
+
33
+ async def persona_from_template(
34
+ client: TavusClient,
35
+ *,
36
+ template: PersonaTemplate,
37
+ persona_name: str | None = None,
38
+ business_context: str | None = None,
39
+ default_replica_id: str | None = None,
40
+ layers: dict[str, Any] | None = None,
41
+ ) -> Any:
42
+ prompt = TEMPLATES[template]
43
+ if business_context:
44
+ prompt = f"{prompt}\n\nBusiness context:\n{business_context.strip()}"
45
+
46
+ body: dict[str, Any] = {
47
+ "persona_name": persona_name or _default_name(template),
48
+ "system_prompt": prompt,
49
+ }
50
+ if default_replica_id:
51
+ body["default_replica_id"] = default_replica_id
52
+ if layers:
53
+ body["layers"] = layers
54
+ return await client.personas.create(body)
55
+
56
+
57
+ def _default_name(template: str) -> str:
58
+ return template.replace("-", " ").title()
@@ -0,0 +1,124 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Callable
4
+ from typing import Any
5
+
6
+ # Leaf path segment -> tool origin, for inline tool arrays on the persona body.
7
+ _ARRAY_LEAF_ORIGIN = {
8
+ "visual_tools": "vision",
9
+ "perception_tools": "vision",
10
+ "audio_tools": "audio",
11
+ }
12
+
13
+ _ADVISORY_FIELDS = ("tool_id", "name", "origin", "description", "parameters", "delivery")
14
+
15
+
16
+ def collect_inline_tools(ops: list[dict[str, Any]]) -> list[dict[str, str]]:
17
+ """Detect inline tool definitions written by JSON-Patch ops.
18
+
19
+ Returns ``[{"name", "origin"}, ...]`` for every inline tool the ops add or
20
+ replace. Handles array writes (``.../visual_tools``, ``.../audio_tools``,
21
+ ``.../perception_tools``, ``/layers/llm/tools``), single appends
22
+ (``.../-`` or ``.../<index>``), and object writes (``/layers``,
23
+ ``/layers/perception``, ``/layers/llm``).
24
+ """
25
+ found: list[dict[str, str]] = []
26
+ seen: set[tuple[str, str]] = set()
27
+
28
+ def add(origin: str, tool: Any) -> None:
29
+ if not isinstance(tool, dict):
30
+ return
31
+ fn = tool.get("function") if isinstance(tool.get("function"), dict) else tool
32
+ name = fn.get("name") if isinstance(fn, dict) else None
33
+ key = (origin, str(name))
34
+ if key in seen:
35
+ return
36
+ seen.add(key)
37
+ found.append({"name": str(name) if name else "", "origin": origin})
38
+
39
+ def add_many(origin: str, value: Any) -> None:
40
+ if isinstance(value, list):
41
+ for tool in value:
42
+ add(origin, tool)
43
+ elif isinstance(value, dict):
44
+ add(origin, value)
45
+
46
+ for op in ops:
47
+ if not isinstance(op, dict) or op.get("op") not in {"add", "replace"}:
48
+ continue
49
+ value = op.get("value")
50
+ segs = [s for s in str(op.get("path", "")).split("/") if s]
51
+ # Drop a trailing append marker / array index so the leaf is the field.
52
+ leaf_segs = [s for s in segs if not (s == "-" or s.isdigit())]
53
+ leaf = leaf_segs[-1] if leaf_segs else ""
54
+
55
+ if leaf in _ARRAY_LEAF_ORIGIN:
56
+ add_many(_ARRAY_LEAF_ORIGIN[leaf], value)
57
+ elif leaf == "tools" and "llm" in leaf_segs:
58
+ add_many("llm", value)
59
+ elif isinstance(value, dict):
60
+ _collect_from_object(leaf, value, add_many)
61
+
62
+ return found
63
+
64
+
65
+ def _collect_from_object(
66
+ leaf: str, value: dict[str, Any], add_many: Callable[[str, Any], None]
67
+ ) -> None:
68
+ """Pull tool arrays out of an object-valued op (a perception layer, an llm
69
+ layer, or a whole ``/layers`` object)."""
70
+ perception = value if leaf == "perception" else value.get("perception")
71
+ if isinstance(perception, dict):
72
+ add_many("vision", perception.get("visual_tools"))
73
+ add_many("vision", perception.get("perception_tools"))
74
+ add_many("audio", perception.get("audio_tools"))
75
+
76
+ llm = value if leaf == "llm" else value.get("llm")
77
+ if isinstance(llm, dict):
78
+ add_many("llm", llm.get("tools"))
79
+
80
+
81
+ def build_inline_deprecation_notice(inline: list[dict[str, str]]) -> dict[str, Any]:
82
+ """Advisory steering a persona patch that wrote inline tools toward the
83
+ first-class create + attach flow."""
84
+ return {
85
+ "inline_tools_written": inline,
86
+ "guidance": (
87
+ "Inline tools (layers.*.tools) are deprecated. Tools are first-class "
88
+ "objects now: create one with tavus_tool_create, then attach it with "
89
+ "tavus_persona_tools_attach (which also flips perception_model to "
90
+ "raven-1 for vision/audio tools). Check tavus_tool_list first so you "
91
+ "reuse an existing tool instead of duplicating it."
92
+ ),
93
+ }
94
+
95
+
96
+ def build_library_match_advisory(
97
+ origin: str, name: str, saved_tools: list[dict[str, Any]]
98
+ ) -> dict[str, Any] | None:
99
+ """Surface saved tools of the same origin as a freshly created tool, so the
100
+ agent reuses instead of duplicating. Returns ``None`` when nothing of that
101
+ origin exists (no advisory noise)."""
102
+ same_origin = [
103
+ tool for tool in saved_tools if isinstance(tool, dict) and tool.get("origin") == origin
104
+ ]
105
+ if not same_origin:
106
+ return None
107
+
108
+ collision = next((tool for tool in same_origin if tool.get("name") == name), None)
109
+ return {
110
+ "new_tool": {"name": name, "origin": origin},
111
+ "name_collision_tool_id": collision.get("tool_id") if collision else None,
112
+ "matching_saved_tools": [_slim(tool) for tool in same_origin],
113
+ "guidance": (
114
+ f"Your account already has {len(same_origin)} saved {origin} tool(s) "
115
+ "(see matching_saved_tools). If one fits this need, do NOT duplicate it "
116
+ "— attach it with tavus_persona_tools_attach instead. If you keep this "
117
+ "new tool, tell the user which existing tool you saw and why its shape "
118
+ "(parameters / origin / delivery) does not fit."
119
+ ),
120
+ }
121
+
122
+
123
+ def _slim(tool: dict[str, Any]) -> dict[str, Any]:
124
+ return {field: tool.get(field) for field in _ADVISORY_FIELDS if field in tool}
@@ -0,0 +1,16 @@
1
+ from tavus_mcp.sdk.schemas.file_manifest import FileEntry, FileManifest
2
+ from tavus_mcp.sdk.schemas.persona import (
3
+ JSONPatchOperation,
4
+ LayersPatchModel,
5
+ PersonaCreate,
6
+ PersonaPatchModel,
7
+ )
8
+
9
+ __all__ = [
10
+ "FileEntry",
11
+ "FileManifest",
12
+ "JSONPatchOperation",
13
+ "LayersPatchModel",
14
+ "PersonaCreate",
15
+ "PersonaPatchModel",
16
+ ]
@@ -0,0 +1,21 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Literal
4
+
5
+ from pydantic import BaseModel, ConfigDict, Field
6
+
7
+
8
+ class FileEntry(BaseModel):
9
+ model_config = ConfigDict(extra="forbid")
10
+
11
+ path: str
12
+ content: str
13
+ mode: Literal["create", "overwrite", "append"] = "create"
14
+ description: str | None = None
15
+
16
+
17
+ class FileManifest(BaseModel):
18
+ model_config = ConfigDict(extra="forbid")
19
+
20
+ files: list[FileEntry] = Field(default_factory=list)
21
+ notes: list[str] = Field(default_factory=list)
@@ -0,0 +1,84 @@
1
+ from __future__ import annotations
2
+
3
+ import re
4
+ from typing import Any, Literal
5
+
6
+ from pydantic import BaseModel, ConfigDict, Field, model_validator
7
+
8
+ GuardrailType = Literal["user", "system", "all"]
9
+ GuardrailModality = Literal["verbal", "visual", "audio"]
10
+ GuardrailListLegacy = Literal["true", "false"]
11
+ GuardrailSort = Literal["ascending", "descending"]
12
+
13
+ MAX_GUARDRAILS_NAME_LENGTH = 100
14
+ MAX_GUARDRAIL_TAGS_PER_ITEM = 32
15
+
16
+ _TAG_NAME_RE = re.compile(r"^[A-Za-z0-9_\- ]+$")
17
+
18
+
19
+ class GuardrailCreate(BaseModel):
20
+ """Flat-list (new) guardrail shape — what `POST /v2/guardrails` accepts
21
+ when the payload has a top-level ``guardrail_name``. Legacy set creation
22
+ (``{name, data: [...]}``) is intentionally not exposed here; the user
23
+ explicitly removed sets in favor of flat guardrails."""
24
+
25
+ model_config = ConfigDict(extra="forbid")
26
+
27
+ guardrail_name: str = Field(..., min_length=1)
28
+ guardrail_prompt: str = Field(..., min_length=1)
29
+ modality: GuardrailModality = "verbal"
30
+ callback_url: str = ""
31
+ tool_call: dict[str, Any] | None = None
32
+ app_message: bool = True
33
+ tags: list[str] = Field(default_factory=list)
34
+
35
+ @model_validator(mode="after")
36
+ def _check_tags(self) -> GuardrailCreate:
37
+ if len(self.tags) > MAX_GUARDRAIL_TAGS_PER_ITEM:
38
+ raise ValueError(
39
+ f"guardrail can carry at most {MAX_GUARDRAIL_TAGS_PER_ITEM} tags"
40
+ )
41
+ for tag in self.tags:
42
+ if not isinstance(tag, str) or not tag:
43
+ raise ValueError("tags must be non-empty strings")
44
+ return self
45
+
46
+
47
+ class GuardrailUpdate(BaseModel):
48
+ """Patch shape — every field optional. RQH's PATCH validator round-trips
49
+ the persisted dict, so persisted fields (uuid, owner_id, timestamps) are
50
+ accepted as no-ops; we just don't expose them as inputs."""
51
+
52
+ model_config = ConfigDict(extra="forbid")
53
+
54
+ guardrail_name: str | None = None
55
+ guardrail_prompt: str | None = None
56
+ modality: GuardrailModality | None = None
57
+ callback_url: str | None = None
58
+ tool_call: dict[str, Any] | None = None
59
+ app_message: bool | None = None
60
+ tags: list[str] | None = None
61
+
62
+ @model_validator(mode="after")
63
+ def _check_tags(self) -> GuardrailUpdate:
64
+ if self.tags is not None and len(self.tags) > MAX_GUARDRAIL_TAGS_PER_ITEM:
65
+ raise ValueError(
66
+ f"guardrail can carry at most {MAX_GUARDRAIL_TAGS_PER_ITEM} tags"
67
+ )
68
+ return self
69
+
70
+
71
+ class GuardrailListParams(BaseModel):
72
+ """Query string for `GET /v2/guardrails`. Defaults mirror RQH so callers
73
+ that omit a param see what RQH would have defaulted them to."""
74
+
75
+ model_config = ConfigDict(extra="forbid")
76
+
77
+ limit: int = 10
78
+ page: int = 1
79
+ name_or_uuid: str = ""
80
+ type: GuardrailType = "user"
81
+ sort: GuardrailSort = "ascending"
82
+ legacy: GuardrailListLegacy = "false"
83
+ tags: str | None = None
84
+ verbose: bool | None = None
@@ -0,0 +1,131 @@
1
+ from __future__ import annotations
2
+
3
+ import re
4
+ from typing import Any, Literal
5
+
6
+ from pydantic import BaseModel, ConfigDict, Field, model_validator
7
+
8
+ ObjectiveType = Literal["user", "system", "all"]
9
+ ConfirmationMode = Literal["manual", "auto"]
10
+ ObjectiveModality = Literal["verbal", "visual", "audio"]
11
+
12
+ MAX_OBJECTIVE_NAME_LEN = 100
13
+ MAX_OBJECTIVE_PROMPT_LEN = 10000
14
+ MAX_OBJECTIVE_CALLBACK_URL_LEN = 2048
15
+ MAX_OBJECTIVE_OUTPUT_VARIABLES = 50
16
+ MAX_OBJECTIVES_NAME_LEN = 100
17
+ MAX_OBJECTIVES_COUNT = 50
18
+
19
+ _OBJECTIVE_NAME_RE = re.compile(r"^[a-zA-Z0-9_]+$")
20
+
21
+
22
+ class ObjectiveItem(BaseModel):
23
+ """One step in an objective chain. Names are referenced by other items
24
+ via ``next_required_objective`` / ``next_conditional_objectives``."""
25
+
26
+ model_config = ConfigDict(extra="forbid")
27
+
28
+ objective_name: str
29
+ objective_prompt: str = Field(..., max_length=MAX_OBJECTIVE_PROMPT_LEN)
30
+ confirmation_mode: ConfirmationMode = "auto"
31
+ output_variables: list[str] = Field(default_factory=list)
32
+ modality: ObjectiveModality = "verbal"
33
+ next_conditional_objectives: dict[str, str] | None = None
34
+ next_required_objective: str | None = None
35
+ callback_url: str = ""
36
+ tool_call: dict[str, Any] | None = None
37
+
38
+ @model_validator(mode="after")
39
+ def _check(self) -> ObjectiveItem:
40
+ if not _OBJECTIVE_NAME_RE.match(self.objective_name):
41
+ raise ValueError(
42
+ "objective_name must contain only letters, numbers, and underscores"
43
+ )
44
+ if len(self.objective_name) > MAX_OBJECTIVE_NAME_LEN:
45
+ raise ValueError(
46
+ f"objective_name must be at most {MAX_OBJECTIVE_NAME_LEN} characters"
47
+ )
48
+ if len(self.callback_url) > MAX_OBJECTIVE_CALLBACK_URL_LEN:
49
+ raise ValueError(
50
+ f"callback_url must be at most {MAX_OBJECTIVE_CALLBACK_URL_LEN} characters"
51
+ )
52
+ if len(self.output_variables) > MAX_OBJECTIVE_OUTPUT_VARIABLES:
53
+ raise ValueError(
54
+ f"output_variables must have at most {MAX_OBJECTIVE_OUTPUT_VARIABLES} items"
55
+ )
56
+ if (
57
+ self.next_required_objective
58
+ and self.next_required_objective == self.objective_name
59
+ ):
60
+ raise ValueError(
61
+ f"objective '{self.objective_name}' cannot reference "
62
+ "itself via next_required_objective"
63
+ )
64
+ if (
65
+ self.next_conditional_objectives
66
+ and self.objective_name in self.next_conditional_objectives
67
+ ):
68
+ raise ValueError(
69
+ f"objective '{self.objective_name}' cannot reference "
70
+ "itself via next_conditional_objectives"
71
+ )
72
+ if self.next_required_objective and self.next_conditional_objectives:
73
+ raise ValueError(
74
+ "an objective may have either next_required_objective or "
75
+ "next_conditional_objectives, not both"
76
+ )
77
+ return self
78
+
79
+
80
+ class ObjectivesCreate(BaseModel):
81
+ """Set-shape body for `POST /v2/objectives/`. Objectives are still
82
+ grouped into a named set (unlike guardrails, which went flat)."""
83
+
84
+ model_config = ConfigDict(extra="forbid")
85
+
86
+ name: str = ""
87
+ data: list[ObjectiveItem] = Field(..., min_length=1)
88
+ allow_loops: bool = False
89
+
90
+ @model_validator(mode="after")
91
+ def _check(self) -> ObjectivesCreate:
92
+ if len(self.name) > MAX_OBJECTIVES_NAME_LEN:
93
+ raise ValueError(
94
+ f"objectives set name must be at most {MAX_OBJECTIVES_NAME_LEN} characters"
95
+ )
96
+ if len(self.data) > MAX_OBJECTIVES_COUNT:
97
+ raise ValueError(
98
+ f"objectives list must have at most {MAX_OBJECTIVES_COUNT} items"
99
+ )
100
+ names = [item.objective_name for item in self.data]
101
+ if len(names) != len(set(names)):
102
+ duplicates = sorted({n for n in names if names.count(n) > 1})
103
+ raise ValueError(f"duplicate objective_name(s) in set: {duplicates}")
104
+ # Cross-item reference check — every referenced name must exist.
105
+ known = set(names)
106
+ for item in self.data:
107
+ if (
108
+ item.next_required_objective
109
+ and item.next_required_objective not in known
110
+ ):
111
+ raise ValueError(
112
+ f"objective '{item.objective_name}' references unknown "
113
+ f"next_required_objective '{item.next_required_objective}'"
114
+ )
115
+ for ref in (item.next_conditional_objectives or {}).keys():
116
+ if ref not in known:
117
+ raise ValueError(
118
+ f"objective '{item.objective_name}' references unknown "
119
+ f"conditional outcome '{ref}'"
120
+ )
121
+ return self
122
+
123
+
124
+ class ObjectivesListParams(BaseModel):
125
+ model_config = ConfigDict(extra="forbid")
126
+
127
+ limit: int = 25
128
+ page: int = 1
129
+ type: ObjectiveType = "user"
130
+ sort: Literal["ascending", "descending"] = "ascending"
131
+ name_or_uuid: str | None = None