from __future__ import annotations
from typing import Any
from pydantic import BaseModel, Field
# ---------------------------------------------------------------------------
# Shared base
# ---------------------------------------------------------------------------
class _NextSteps(BaseModel):
"""Mixin that adds next_steps hints to every response."""
next_steps: list[str] = Field(
default_factory=list,
description="Suggested follow-up tool calls to continue your investigation.",
)
# ---------------------------------------------------------------------------
# Discovery
# ---------------------------------------------------------------------------
class OperationMatch(BaseModel):
"""A single matching API operation returned by find_operation."""
operation_id: str = Field(description="Pass this to execute_operation() to run it.")
method: str = Field(description="HTTP method: GET, POST, DELETE, etc.")
path: str = Field(description="API path template (may contain {param} placeholders).")
summary: str = Field(description="Short description of what this operation does.")
service: str = Field(description="API service / tag this operation belongs to.")
schema_resource: str = Field(
description="MCP resource URI for the full resolved schema. Read this resource to get parameters and request body details.",
)
full_schema: dict[str, Any] | None = Field(
default=None,
description="Full resolved schema (parameters, request body, responses). "
"Included when find_operation is called with include_schema=True.",
)
class OperationSearchResult(_NextSteps):
"""Result from find_operation."""
matches: list[OperationMatch] = Field(description="Ranked list of matching operations.")
total_searched: int = Field(description="Total number of operations searched.")
# ---------------------------------------------------------------------------
# Account
# ---------------------------------------------------------------------------
class AccountInfo(_NextSteps):
"""Current Unblu account information."""
id: str
name: str | None = None
next_steps: list[str] = Field(
default_factory=lambda: [
"Call search_conversations() to list active conversations.",
"Call search_persons() to find agents or visitors.",
"Call check_agent_availability() to see who is online.",
],
)
# ---------------------------------------------------------------------------
# Conversations
# ---------------------------------------------------------------------------
class ConversationParticipant(BaseModel):
"""A participant in a conversation."""
person_id: str
participation_type: str | None = None
state: str | None = None
hidden: bool = False
class ConversationSummary(BaseModel):
"""Compact conversation info for list results."""
id: str
topic: str | None = None
state: str = Field(description="EConversationState: CREATED, QUEUED, ACTIVE, UNASSIGNED, ENDED, etc.")
created_at: int | None = Field(default=None, description="Unix ms timestamp.")
ended_at: int | None = Field(default=None, description="Unix ms timestamp. Null if not ended.")
awaited_person_type: str | None = Field(default=None, description="AGENT, VISITOR, or NONE.")
participant_count: int = 0
bot_participant_count: int = 0
source_url: str | None = None
class ConversationPage(_NextSteps):
"""Paginated list of conversations."""
items: list[Any]
has_more: bool = False
next_offset: int | None = Field(
default=None,
description="Pass as offset= on your next call to get the next page.",
)
class ConversationDetail(_NextSteps):
"""Full conversation details for debugging."""
id: str
topic: str | None = None
state: str
created_at: int | None = None
ended_at: int | None = None
visibility: str | None = None
locale: str | None = None
awaited_person_type: str | None = None
source_url: str | None = None
source_id: str | None = None
initial_engagement_type: str | None = None
end_reason: str | None = None
participants: list[ConversationParticipant] = Field(default_factory=list)
bot_participant_count: int = 0
metadata: dict[str, Any] | None = None
gui_url: str | None = Field(default=None, description="Unblu admin console URL for this conversation.")
# ---------------------------------------------------------------------------
# Persons
# ---------------------------------------------------------------------------
class PersonSummary(BaseModel):
"""Compact person info for list results."""
id: str
display_name: str | None = None
person_type: str | None = Field(default=None, description="AGENT, VISITOR, BOT, or SYSTEM.")
email: str | None = None
team_id: str | None = None
authorization_role: str | None = None
class PersonPage(_NextSteps):
"""Paginated list of persons."""
items: list[Any]
has_more: bool = False
next_offset: int | None = Field(
default=None,
description="Pass as offset= on your next call to get the next page.",
)
class PersonDetail(_NextSteps):
"""Full person details for debugging."""
id: str
display_name: str | None = None
person_type: str | None = None
email: str | None = None
phone: str | None = None
username: str | None = None
team_id: str | None = None
labels: list[str] = Field(default_factory=list)
note: str | None = None
authorization_role: str | None = None
source_id: str | None = None
source_url: str | None = None
gui_url: str | None = Field(default=None, description="Unblu admin console URL for this person.")
class PersonAmbiguousResult(_NextSteps):
"""Returned when get_person finds multiple candidates."""
message: str = "Multiple persons matched. Call get_person() again with the exact person_id from candidates."
candidates: list[PersonSummary]
# ---------------------------------------------------------------------------
# Users (admin accounts, distinct from Persons)
# ---------------------------------------------------------------------------
class UserSummary(BaseModel):
"""Compact user info for list results."""
id: str
username: str | None = None
display_name: str | None = None
email: str | None = None
authorization_role: str | None = None
class UserPage(_NextSteps):
"""Paginated list of users."""
items: list[Any]
has_more: bool = False
next_offset: int | None = Field(
default=None,
description="Pass as offset= on your next call to get the next page.",
)
class UserDetail(_NextSteps):
"""Full user details."""
id: str
username: str | None = None
display_name: str | None = None
email: str | None = None
phone: str | None = None
team_id: str | None = None
authorization_role: str | None = None
virtual_user: bool | None = None
externally_managed: bool | None = None
gui_url: str | None = Field(default=None, description="Unblu admin console URL for this user.")
# ---------------------------------------------------------------------------
# Persons — batch lookup
# ---------------------------------------------------------------------------
class PersonBatchEntry(BaseModel):
"""One entry in a get_persons() batch result."""
identifier: str = Field(description="The identifier that was looked up.")
result: PersonDetail | PersonAmbiguousResult | None = None
error: str | None = Field(default=None, description="Error message if the lookup failed.")
class PersonBatchResult(_NextSteps):
"""Result from get_persons() batch lookup."""
entries: list[PersonBatchEntry]
total: int = Field(description="Total number of identifiers requested.")
succeeded: int = Field(description="Number of successful lookups.")
failed: int = Field(description="Number of failed lookups (not found or error).")
# ---------------------------------------------------------------------------
# Availability
# ---------------------------------------------------------------------------
class AvailabilityInfo(_NextSteps):
"""Agent availability status."""
named_area_site_id: str | None = None
availability: str | None = Field(
default=None,
description="Raw availability value from the Unblu API.",
)
raw: dict[str, Any] = Field(
default_factory=dict,
description="Full raw availability response for additional fields.",
)
# ---------------------------------------------------------------------------
# Mutations
# ---------------------------------------------------------------------------
class OperationResult(_NextSteps):
"""Result of a write operation (assign, end, etc.)."""
success: bool
message: str
conversation_id: str | None = None
# ---------------------------------------------------------------------------
# Escape hatch (execute_operation)
# ---------------------------------------------------------------------------
class ExecuteResult(_NextSteps):
"""Result from execute_operation."""
status_code: int
data: Any = None
has_more: bool | None = None
next_offset: int | None = Field(
default=None,
description="Pass as offset= on your next call to get the next page.",
)
truncated: bool = False