Documentação

Referência da API

Tudo nesta página é importável de laya (os nomes comuns) ou de laya.hooks (toda a superfície).

from laya import PredictContext, PredictHook, Hook
from laya.hooks import HOOK_EVENTS, normalise_hooks, dispatch, aggregate_usage

PredictContext

Um PredictContext é criado uma vez por chamada pública e passado a cada hook dessa chamada. O Router.predict_batch é a única exceção: cria um por pedido, como faria uma chamada Router.predict para esse pedido, por isso os seus hooks ao nível do Router disparam uma vez por pedido com um estado cada. É mutável: os hooks podem reescrever states, questions e results, e o on_route pode reescrever decision. Usa igualdade por identidade (eq=False), por isso um contexto é hasheável e dois contextos nunca são iguais.

@dataclass(eq=False)
class PredictContext:
    states: List[Any]
    questions: Dict[str, Any]
    run_id: str = <uuid4 hex>
    results: Optional[List[Dict[str, Any]]] = None
    decision: Optional[Dict[str, Any]] = None
    model: Optional[str] = None
    agent: Any = None
    router: Any = None
    max_len: Optional[int] = None
    head_max_len: Optional[int] = None
    usage: Optional[Dict[str, int]] = None
    started_at: float = <perf_counter()>
    elapsed_ms: Optional[float] = None
    error: Optional[BaseException] = None
campo tipo definido quando mutável significado
states list sempre sim (início) os estados para esta chamada. system_one/Router.predict passam um; Agent.predict_batch passa muitos; Router.predict_batch passa um por pedido. Um hook de início pode substituir a lista.
questions dict sempre sim (início) as perguntas. Um hook de início pode substituir o dict.
run_id str sempre não um id único partilhado por cada hook desta chamada. Usa-o para correlacionar eventos e spans.
results list | None fim (e num skip) sim (fim) dicts de resultado por estado, cada um com a forma do retorno de system_one. None até a inferência terminar.
decision dict | None apenas Router sim (route) o RouteDecision (um dict) que selecionou o checkpoint.
model str | None sempre não o id do checkpoint: Agent.model_id para um Agent, o alias resolvido (por exemplo "english") para um Router.
agent Agent | ONNXAgent | None eventos de predição não o runtime que responde à chamada.
router Router | None eventos de Router não o Router, quando um está em jogo.
max_len int | None sempre sim (início) orçamento de tokens por chamada para o encoder. None usa a config do agente.
head_max_len int | None sempre sim (início) orçamento de tokens por chamada para a cabeça de pergunta. None usa a config do agente.
usage dict | None fim sim (fim) {"input_tokens", "output_tokens"}, somados sobre os estados da chamada.
started_at float sempre não time.perf_counter() quando a chamada começou.
elapsed_ms float | None fim não tempo de relógio para toda a chamada, em milissegundos.
error BaseException | None caminho de falha não a exceção, definida antes de on_error e on_predict_end.

PredictContext.skip(results)

Faz curto-circuito na inferência. Chamado a partir de on_predict_start, define ctx.results para que a passagem direta seja saltada; o on_predict_end continua a correr e os resultados fornecidos são devolvidos.

A chave tem de cobrir tudo de que a resposta depende, e o hook tem de cobrir tudo o que a chamada transporta: os hooks disparam uma vez por chamada, e o predict_batch chama-os com todos os estados ao mesmo tempo.

CACHE = {}

def key(ctx, index):
    # Not sort_keys=True: criteria order is positional, so two orders are two questions,
    # and the checkpoint and token budget change the answer too.
    payload = json.dumps([ctx.states[index], ctx.questions, ctx.model,
                          ctx.max_len, ctx.head_max_len], default=str)
    return hashlib.sha256(payload.encode()).hexdigest()

def cache_read(ctx):
    hits = [CACHE.get(key(ctx, i)) for i in range(len(ctx.states))]
    if all(hit is not None for hit in hits):
        ctx.skip(hits)   # one entry per state, same shape as predict_batch's return

def cache_write(ctx):
    for i, result in enumerate(ctx.results or []):
        CACHE[key(ctx, i)] = result

laya.load("convaiinnovations/laya", on_predict_start=cache_read, on_predict_end=cache_write)

O tests/test_hooks_api.py executa este bloco, o examples/hooks/cache.py e os blocos de cache de docs/hooks/patterns.md e docs/hooks/examples.md, e afirma as quatro chaves da mesma forma, para que uma página não possa ensinar uma chave de que o exemplo já se afastou.

No Router, um payload saltado recebe uma chave routing acrescentada (sem sobrepor uma que já tenha), para que Router.predict mantenha a sua forma de retorno documentada.

Protocolo de Hook

Hook é um typing.Protocol. Implementa qualquer subconjunto dos métodos; o resto é saltado.

class Hook(Protocol):
    def on_predict_start(self, ctx: PredictContext) -> None: ...
    def on_predict_end(self, ctx: PredictContext) -> None: ...
    def on_route(self, ctx: PredictContext) -> None: ...
    def on_load(self, ctx: PredictContext) -> None: ...
    def on_evict(self, ctx: PredictContext) -> None: ...
    def on_error(self, ctx: PredictContext) -> None: ...
evento onde corre pode alterar
on_predict_start Agent, Router antes da tokenização/passagem direta states, questions, ou skip()
on_predict_end Agent, Router depois de existirem resultados, sucesso ou falha results
on_route Router depois da deteção, antes do carregamento decision
on_load Router depois de um checkpoint ser construído nada (observar)
on_evict Router depois de um checkpoint ser libertado nada (observar)
on_error Agent, Router quando uma chamada de predição falha nada (observar)

Um hook é livre de definir atributos e métodos extra; só os seis nomes de evento são consultados. Se um hook definir um dos seis como não-invocável, a configuração falha depressa (vê Validação).

BaseHook

BaseHook é a contraparte concreta do protocolo: uma classe com um corpo sem operação para cada evento. Faz-lhe subclasse e sobrepõe apenas os eventos de que precisas.

from laya import BaseHook

class Audit(BaseHook):
    def on_predict_end(self, ctx):
        ship(ctx.run_id, ctx.results)

Hook é melhor quando queres tipagem estrutural (qualquer objeto com os métodos certos); BaseHook é melhor quando queres uma base explícita para fazer subclasse e chamar super().

Tipos de conveniência

PredictHook = Callable[[PredictContext], None]

PredictHook é o tipo de um invocável simples usado com on_predict_start= / on_predict_end=. Passa um único invocável ou uma sequência deles; cada um é envolvido num hook mínimo.

Registo em runtime

Cada runtime incorpora o HookRegistry, por isso os hooks podem ser acrescentados, removidos ou limitados após a construção. A mutação é thread-safe; uma chamada lê um snapshot da lista, por isso acrescentar ou remover um hook nunca perturba uma chamada em voo.

agent.add_hook(tracer)              # one hook or a sequence; returns self for chaining
agent.remove_hook(tracer)           # by identity; True if it was installed

with agent.hooks_installed(debug):  # installed for the block, removed on exit
    agent.system_one(state, questions)

add_hook aceita os mesmos objetos que hooks= (não invocáveis simples). hooks_installed aceita qualquer número de objetos hook ou sequências e restaura a lista anterior à saída, incluindo quando o bloco levanta exceção.

Predefinições para todo o processo

O laya.hooks mantém um pequeno registo para todo o processo, para que um tracer, hook de métricas ou etiquetador de tenant não tenha de ser passado por todos os Agent e Router. As predefinições correm primeiro, depois os hooks instalados na instância, depois os hooks por chamada.

from laya import hooks

hooks.set_default_hooks(hooks=[Tracer()])          # replaces the set, accepts the hooks= arguments
hooks.add_default_hook(Metrics())                  # appends
hooks.clear_default_hooks()                        # removes everything
hooks.default_hooks()                              # a copy of the current list

hooks.compose_hooks(agent.hooks)                   # defaults + installed (advanced)

As predefinições aplicam-se a cada evento, incluindo os eventos de ciclo de vida do Router on_load e on_evict. O registo é lido no momento da chamada, por isso os hooks definidos depois de um Agent ou Router ser construído ainda se aplicam. Não há opt-out por instância; chama clear_default_hooks() para desligar o conjunto para todo o processo.

Hooks assíncronos

Um evento pode ser uma corrotina. Envolve o hook em AsyncHook e os seus métodos async def correm até ao fim no core síncrono:

from laya import AsyncHook

class Remote:
    async def on_predict_end(self, ctx):
        await ship(ctx.results)

agent = laya.load("convaiinnovations/laya", hooks=[AsyncHook(Remote())])

Um invocável assíncrono simples passado a on_predict_start= / on_predict_end= também funciona, porque o dispatch corre qualquer awaitable que um hook devolva.

Onde a corrotina corre:

  • Se a thread que chama não tem um ciclo em execução, corre com asyncio.run.
  • Se já tem um (um autor de chamada dentro de uma função async), corre num ciclo de fundo dedicado, para que a thread que chama possa bloquear sem impasse. Passa AsyncHook(hook, loop=...) para encaminhar para um ciclo específico; tem de estar em execução, e não pode ser o próprio ciclo da thread que chama. Ambos são verificados: um ciclo parado e o próprio ciclo do autor da chamada levantam, cada um, ValueError em vez de bloquear para sempre.

Um hook sem métodos async não é afetado.

Superfície de configuração

Cada ponto de entrada aceita os mesmos parâmetros de hook. hooks aceita um objeto ou uma sequência de objetos; on_predict_start / on_predict_end aceitam um invocável ou uma sequência.

parâmetro tipo predefinição significado
hooks Hook | Sequence[Hook] | None None hooks de ciclo de vida (qualquer dos seis eventos).
on_predict_start PredictHook | Sequence[PredictHook] | None None invocáveis de conveniência para um evento.
on_predict_end PredictHook | Sequence[PredictHook] | None None invocáveis de conveniência para um evento.
hooks_raise bool True True: uma exceção de um hook propaga-se. False: avisa e continua.
hooks_concurrent bool True False: despacha os hooks sob um lock, um de cada vez.
hooks_timeout float | None None limite de tempo por hook em segundos; None significa sem limite.

Agent

Agent(
    model_id_or_path="convaiinnovations/laya",
    device=None, token=None, subfolder=None, fast=False, compile=False,
    hooks=None, on_predict_start=None, on_predict_end=None,
    hooks_raise=True, hooks_concurrent=True, hooks_timeout=None,
)

load(..., hooks=None, on_predict_start=None, on_predict_end=None,
     hooks_raise=True, hooks_concurrent=True, hooks_timeout=None)

agent.predict_batch(states, questions, batch_size=None,
                    hooks=None, on_predict_start=None, on_predict_end=None, hooks_raise=None,
                    hooks_timeout=None, max_len=None, head_max_len=None, sort_by_length=False)

agent.system_one(state, questions,
                 hooks=None, on_predict_start=None, on_predict_end=None, hooks_raise=None,
                 hooks_timeout=None, max_len=None, head_max_len=None)

agent.predict_long(state, questions, window=None, stride=None, aggregate="auto",
                   batch_size=None, lang=None,
                   hooks=None, on_predict_start=None, on_predict_end=None, hooks_raise=None,
                   hooks_timeout=None)

agent.predict(...)          # alias of system_one
  • hooks_raise e hooks_timeout num método por chamada assumem None por predefinição, o que significa «usar o valor da instância».

  • hooks_concurrent é apenas ao nível da instância.

  • No predict_long os hooks envolvem a inferência que responde ao estado, que para um documento que precisa de várias janelas é o único predict_batch partilhado sobre elas: o on_predict_start dispara uma vez, e o ctx.states contém os textos das janelas descodificados pela ordem de varrimento, e não o estado do autor da chamada, que foi tokenizado para os produzir. O states é mutável a partir de um hook de início, por isso o varrimento que chega à inferência não tem de ser o split que o predict_long calculou. O que muda é aquilo que a resposta pode alegar:

    o que o hook de início fez usage["windows"] answer["window"]
    respondeu com ctx.skip([result]) 0 ausente – nenhuma janela o pontuou
    deixou o varrimento tal como foi construído N presente – index, token_start/token_end nomeiam o span decisivo
    substituiu o varrimento, de qualquer forma os estados que foram pontuados ausente – os offsets descrevem as janelas do predict_long, não o texto que foi pontuado

    usage["windows"] é o total em todos os caminhos, incluindo aquele em que um estado já cabia numa única janela, por isso uma resposta em cache nunca se lê como uma janela que o modelo leu.

Router

Router(
    models=None, device=None, token=None, max_loaded=2, default="english",
    auto_task_detection=False, standalone_repos=False, preload=False, lang_guess=None,
    hooks=None, on_predict_start=None, on_predict_end=None,
    hooks_raise=True, hooks_concurrent=True,
)

router.route(state, questions=None, model=None, task=None, lang=None, lang_guess=None,
             hooks=None, hooks_raise=None)

router.predict(state, questions, model=None, task=None, lang=None, lang_guess=None,
               hooks=None, on_predict_start=None, on_predict_end=None, hooks_raise=None,
               hooks_timeout=None, max_len=None, head_max_len=None)

router.predict_batch(requests, batch_size=None, hooks_timeout=None, min_confidence=None,
                     sort_by_length=False, hooks=None, on_predict_start=None, on_predict_end=None,
                     hooks_raise=None)

router.system_one(...)      # alias of predict
router.load(name)           # builds on first use; fires on_load
router.preload(names=None)  # builds several; fires on_load per build
router.unload(name=None)    # frees one or all; fires on_evict
router.attach(name, agent)  # registers an existing agent; does not fire on_load
router.loaded               # list of resident checkpoint names
  • hooks= por chamada em route, route_batch, predict e predict_batch aplicam-se a toda a chamada, incluindo o on_route. Em predict_batch a lista é composta da mesma forma que em predict (primeiro os hooks instalados, depois a lista por chamada; None e [] não acrescentam nada) e corre uma vez por pedido.
  • route() é público: chamá-lo despacha o on_route com os hooks instalados mais quaisquer hooks por chamada.

ONNXAgent

ONNXAgent(model_id_or_path, onnx_path="laya.onnx", subfolder=None,
          hooks=None, on_predict_start=None, on_predict_end=None,
          hooks_raise=True, hooks_concurrent=True)

onnx_agent.system_one(state, questions,
                      hooks=None, on_predict_start=None, on_predict_end=None, hooks_raise=None,
                      hooks_timeout=None, max_len=None, head_max_len=None)

onnx_agent.predict(...)     # alias of system_one

O ONNXAgent não tem Router, por isso expõe apenas os eventos ao nível da predição.

Payloads de eventos

Que campos são preenchidos, por evento e runtime:

evento runtime states questions decision model agent router results usage elapsed_ms error
on_predict_start Agent ✓ ✓ – ✓ ✓ – – – – –
on_predict_start Router ✓ ✓ ✓ ✓ ✓ ✓ – – – –
on_predict_end Agent ✓ ✓ – ✓ ✓ – ✓ em sucesso ✓ em falha
on_predict_end Router ✓ ✓ ✓ ✓ ✓ ✓ ✓ em sucesso ✓ em falha
on_error ambos ✓ ✓ ✓ (Router) ✓ ✓ ✓ (Router) – – – ✓
on_route Router ✓ ✓ ✓ – – ✓ – – – –
on_load Router [] {} – ✓ ✓ ✓ – – – –
on_evict Router [] {} – ✓ – ✓ – – – –

Detalhes de temporização:

  • on_predict_end vê results no caminho de sucesso. No caminho de falha, results é None a menos que um hook de início os tenha definido via skip(), e usage é, portanto, também None (é derivado de results); elapsed_ms é sempre definido.
  • on_error corre antes do bloco finally que calcula elapsed_ms e usage, por isso ambos são None aí. Lê antes a temporização e o usage a partir de on_predict_end.
  • run_id é sempre preenchido.

Validação

A configuração é validada quando os hooks são normalizados, o que acontece na construção para os hooks instalados e no momento da chamada para os hooks por chamada. Estes levantam TypeError:

caso mensagem
é passada uma classe em vez de uma instância hooks entries must be instances, not classes; ...
um objeto não implementa nenhum dos seis eventos hooks entries must implement at least one of ...
um atributo de evento não é invocável hooks entry X.on_predict_start must be callable, got int
on_predict_start= / on_predict_end= não é invocável on_predict_start must be callable, got int

hooks= não aceita invocáveis simples, porque um invocável nu não diz para que evento é. Usa on_predict_start= / on_predict_end= para esses.

Ajudantes avançados

Estes são usados internamente e são estáveis, mas a maioria dos utilizadores não precisa deles.

HOOK_EVENTS          # tuple of the six event names, in dispatch order
normalise_hooks(hooks=None, on_predict_start=None, on_predict_end=None) -> list
dispatch(hooks, event, ctx, *, raise_errors=True, lock=None) -> None
aggregate_usage(results) -> {"input_tokens": int, "output_tokens": int}
dispatch(hooks, event, ctx, *, raise_errors=True, lock=None, timeout=None)
run_coroutine_sync(coro, loop=None)

normalise_hooks achata um objeto/sequência hooks e os dois invocáveis numa única lista ordenada. dispatch chama event em cada hook que o implementa, aplicando a política de exceções, o lock e o timeout, e corre o resultado de um hook se for awaitable. run_coroutine_sync corre um awaitable até ao fim a partir de código síncrono, no ciclo do autor da chamada se estiver livre, ou num ciclo de fundo se o autor da chamada já tiver um. aggregate_usage soma blocos de usage por estado.

from laya.hooks import normalise_hooks, dispatch, PredictContext

hooks = normalise_hooks(on_predict_start=[log, redact])
ctx = PredictContext(states=["..."], questions={...})
dispatch(hooks, "on_predict_start", ctx)

Ver também