Skill Selector
O SkillSelector seleciona automaticamente a skill externa mais adequada para o contexto da conversa usando o LLM. Use quando o agente tem múltiplas skills externas e precisa decidir para qual encaminhar o usuário.
Inicialização
from actions_sdk.core.types.action_context import ActionContext
from actions_sdk.features.conversational.skill_selector import SkillSelector
ctx: ActionContext
selector = SkillSelector(ctx)
# Com timezone para verificação de horário de atendimento
selector = SkillSelector(ctx, timezone="America/Sao_Paulo")
O SkillSelector extrai ctx.external_skills e ctx.messages automaticamente no construtor, e cria internamente um LLMClient.
Parâmetros do construtor:
| Parâmetro | Tipo | Padrão | Descrição |
|---|---|---|---|
ctx | ActionContext | — | Contexto de execução |
timezone | str | "UTC" | Fuso horário para verificação de horário de atendimento das skills |
Métodos
select_best_skill()
Seleciona a skill mais adequada para o contexto da conversa usando LLM.
selector = SkillSelector(ctx)
skill_selecionada = selector.select_best_skill()
if skill_selecionada:
return ResponseToAgent(
status=ResponseStatus.SUCCESS,
instruction=f"Encaminhar o usuário para: {skill_selecionada.name}",
functions=[RedirectToSkill(
skill_name=skill_selecionada.name,
reason="Skill selecionada automaticamente.",
)],
)
Parâmetros:
| Parâmetro | Tipo | Descrição |
|---|---|---|
external_skills | list[ExternalSkillConfig] | None | Skills candidatas. Se None, usa as skills do construtor (ctx.external_skills) |
messages | list[Message] | None | Histórico da conversa. Se None, usa ctx.messages |
context_hint | str | None | Dica adicional de contexto para auxiliar a seleção |
fallback_skill | ExternalSkillConfig | None | Skill de último recurso se nenhuma outra se encaixar |
Retorna: ExternalSkillConfig | None
Se a seleção falhar (LLM indisponível, erro interno), retorna None silenciosamente — não lança exceção.
get_available_external_skills()
Retorna todas as skills externas passadas na abertura do SkillSelector (ctx.external_skills), sem filtrar por horário de atendimento.
Use este método para obter a lista completa de skills configuradas. Para filtrar pelas que estão abertas agora, chame get_external_skills_availability() a seguir.
todas_as_skills = selector.get_available_external_skills()
print([s.name for s in todas_as_skills])
Retorna: list[ExternalSkillConfig]
get_external_skill_availability(external_skill)
Verifica a disponibilidade de uma única skill e retorna o motivo do fechamento e a mensagem de exceção configurada, quando houver. Método preferido para checagem antes de transbordo.
from actions_sdk import SkillSelector
selector = SkillSelector(ctx, timezone="America/Sao_Paulo")
availability = selector.get_external_skill_availability(skill)
if not availability.available:
if availability.closed_reason == "exception" and availability.message:
# Exibe a mensagem que a operação configurou para o período de exceção.
# O Force bloqueia a transferência de qualquer forma — a checagem aqui
# existe para o usuário receber a mensagem em vez de um bloqueio sem explicação.
return ResponseToUser(
instruction="Skill fechada por exceção — mensagem configurada exibida ao usuário",
message=availability.message,
)
Parâmetros:
| Parâmetro | Tipo | Descrição |
|---|---|---|
external_skill | ExternalSkillConfig | A skill a verificar |
Retorna: SkillAvailability
get_external_skills_availability(external_skills=None)
Retorna a disponibilidade de cada skill externa, incluindo o motivo do fechamento e a mensagem de exceção configurada. Avalia a lista inteira numa passada.
from actions_sdk import SkillAvailability
status = selector.get_external_skills_availability()
for item in status:
if not item.available and item.closed_reason == "exception" and item.message:
print(f"{item.skill.name}: fechada por exceção — {item.message}")
Parâmetros:
| Parâmetro | Tipo | Descrição |
|---|---|---|
external_skills | list[ExternalSkillConfig] | None | Skills a avaliar. Se None, usa as skills do construtor (ctx.external_skills) |
Retorna: list[SkillAvailability], uma entrada por skill, na mesma ordem de entrada.
is_external_skill_available(external_skill) (depreciado)
Depreciado — permanece funcional com assinatura e retorno
boolintactos (actions existentes não quebram). Useget_external_skill_availability()em actions novas.
disponivel = selector.is_external_skill_available(skill) # bool
Retorna: bool
SkillAvailability
Resultado da verificação de disponibilidade de uma skill:
| Campo | Tipo | Descrição |
|---|---|---|
skill | ExternalSkillConfig | A skill avaliada |
available | bool | Se a skill está disponível agora |
closed_reason | "opening_hours" | "exception" | None | Motivo do fechamento; None quando disponível |
message | str | None | Mensagem de exceção configurada (terceiro elemento da janela opening_hours_exceptions), presente quando closed_reason == "exception" e a janela que casou carrega mensagem |
ExternalSkillConfig
Estrutura que representa uma skill externa:
| Campo | Tipo | Descrição |
|---|---|---|
name | str | Nome da skill |
description | str | Descrição usada pelo LLM para seleção |
opening_hours | dict | None | Horários de atendimento por dia da semana |
opening_hours_exceptions | list[list[str]] | None | Exceções de horário. Cada janela é [início_iso, fim_iso] ou [início_iso, fim_iso, mensagem]. O terceiro elemento — mensagem opcional — é exibido ao usuário quando a skill está fechada por aquela exceção |
Erros e Exceções
O SkillSelector é fail-safe: todos os erros internos são capturados e retornam None em vez de lançar exceções. Verifique sempre se o retorno de select_best_skill() é None antes de usar.
skill = selector.select_best_skill()
if skill is None:
return ResponseToAgent(
status=ResponseStatus.ERROR,
instruction="Não foi possível selecionar uma skill automaticamente.",
)
Exemplos práticos
Listar skills com disponibilidade atual
from actions_sdk.features.conversational.skill_selector import SkillSelector
ctx: ActionContext
selector = SkillSelector(ctx, timezone="America/Sao_Paulo")
# Todas as skills configuradas (sem filtro de horário)
todas = selector.get_available_external_skills()
# Disponibilidade de cada uma
status = selector.get_external_skills_availability()
for item in status:
estado = "aberta" if item.available else f"fechada ({item.closed_reason})"
print(f"{item.skill.name}: {estado}")
Exibir mensagem de exceção antes de transferir
Quando uma skill está fechada por um período de exceção configurado (ex.: feriado), a mensagem cadastrada pode ser exibida diretamente ao usuário.
selector = SkillSelector(ctx, timezone="America/Sao_Paulo")
# Supondo que a skill já foi identificada pelo nome ou por seleção manual
skill_alvo = next(
(s for s in selector.get_available_external_skills() if s.name == "Suporte Técnico"),
None,
)
if skill_alvo is None:
return ResponseToAgent(
status=ResponseStatus.ERROR,
instruction="Skill não encontrada.",
)
availability = selector.get_external_skill_availability(skill_alvo)
if not availability.available:
if availability.closed_reason == "exception" and availability.message:
# Exibe a mensagem de exceção que a operação configurou (ex.: "Fechado no feriado de Natal")
return ResponseToUser(
instruction="Skill fechada por exceção — mensagem configurada exibida ao usuário",
message=availability.message,
)
# Fechada por horário normal
return ResponseToUser(
instruction="Skill fora do horário de atendimento",
message="O suporte técnico está indisponível no momento. Tente novamente em horário comercial.",
)
# Skill disponível — prosseguir com a transferência
return ResponseToAgent(
status=ResponseStatus.SUCCESS,
instruction=f"Transferir para {skill_alvo.name}.",
functions=[RedirectToSkill(skill_name=skill_alvo.name, reason="Solicitado pelo usuário.")],
)
Filtrar skills disponíveis antes de selecionar a melhor
O fluxo recomendado quando há múltiplas skills: checar disponibilidade primeiro e só então passar a lista filtrada para o LLM.
selector = SkillSelector(ctx, timezone="America/Sao_Paulo")
status = selector.get_external_skills_availability()
# Separar abertas das fechadas
abertas = [item.skill for item in status if item.available]
fechadas_por_excecao = [item for item in status if item.closed_reason == "exception" and item.message]
if not abertas:
# Nenhuma skill aberta — verificar se há mensagem de exceção para exibir
if fechadas_por_excecao:
mensagens = " | ".join(item.message for item in fechadas_por_excecao)
return ResponseToUser(
instruction="Skills fechadas por exceção",
message=mensagens,
)
return ResponseToUser(
instruction="Todas as skills estão fora do horário de atendimento",
message="Nosso atendimento especializado está indisponível no momento.",
)
# Selecionar a melhor entre as abertas
skill = selector.select_best_skill(external_skills=abertas)
if skill is None:
return ResponseToAgent(
status=ResponseStatus.ERROR,
instruction="Nenhuma skill adequada encontrada.",
)
return ResponseToAgent(
status=ResponseStatus.SUCCESS,
instruction=f"Encaminhar o usuário para: {skill.name}",
functions=[RedirectToSkill(skill_name=skill.name, reason="Skill selecionada automaticamente.")],
)
Exemplo completo
Baseado na action skill_selector_selecionar do agente de homologação:
import traceback
from actions_sdk.core.types.action_context import ActionContext
from actions_sdk.core.types.responses import (
RedirectToSkill,
ResponseStatus,
ResponseToAgent,
)
from actions_sdk.features.conversational.skill_selector import SkillSelector
from actions_sdk.features.conversational.state import StateManager
from actions_sdk.features.tracing import ActionSpan
ctx: ActionContext
CHAVE_PROGRESSO = "action.skill_selector.progresso"
def selecionar_skill():
span = ActionSpan(ctx)
state = StateManager(ctx)
span.event("action.skill_selector.inicio", {})
try:
selector = SkillSelector(ctx, timezone="America/Sao_Paulo")
skill = selector.select_best_skill(
context_hint="O usuário parece ter uma dúvida técnica.",
)
if skill is None:
state.update(CHAVE_PROGRESSO, "SEM_SKILL")
return ResponseToAgent(
status=ResponseStatus.ERROR,
instruction="Nenhuma skill adequada encontrada para o contexto atual.",
state_updates=state.get_full_state(),
)
state.update(CHAVE_PROGRESSO, "SUCESSO ✓")
span.event("action.skill_selector.sucesso", {"skill": skill.name})
return ResponseToAgent(
status=ResponseStatus.SUCCESS,
instruction=f"Skill selecionada: {skill.name}. Encaminhar o usuário.",
functions=[
RedirectToSkill(
skill_name=skill.name,
reason=f"Skill '{skill.name}' selecionada automaticamente por LLM.",
)
],
state_updates=state.get_full_state(),
)
except Exception as erro:
state.update(CHAVE_PROGRESSO, "ERRO ✗")
span.event("action.skill_selector.erro", {"erro": str(erro)})
return ResponseToAgent(
status=ResponseStatus.ERROR,
instruction=f"Falha no seletor de skill: {traceback.format_exc()}",
state_updates=state.get_full_state(),
)