Documentation

Référence de l’API

Tout sur cette page s’importe depuis laya (les noms courants) ou laya.hooks (toute la surface).

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

PredictContext

Un PredictContext est créé une fois par appel public et passé à chaque hook de cet appel. Router.predict_batch est la seule exception : il en crée un par requête, comme le ferait un appel Router.predict pour cette requête, donc ses hooks au niveau Router se déclenchent une fois par requête avec un état chacun. Il est mutable : les hooks peuvent réécrire states, questions et results, et on_route peut réécrire decision. Il utilise l’égalité par identité (eq=False), donc un contexte est hachable et deux contextes ne sont jamais égaux.

@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
champ type défini quand mutable signification
states list toujours oui (start) les états de cet appel. system_one/Router.predict en passent un ; Agent.predict_batch en passe plusieurs ; Router.predict_batch en passe un par requête. Un hook de start peut remplacer la liste.
questions dict toujours oui (start) les questions. Un hook de start peut remplacer le dict.
run_id str toujours non un id unique partagé par chaque hook de cet appel. Utilise-le pour corréler les événements et les spans.
results list | None end (et lors d’un skip) oui (end) les dicts de résultat par état, chacun de la forme du retour de system_one. None jusqu’à la fin de l’inférence.
decision dict | None Router uniquement oui (route) le RouteDecision (un dict) qui a sélectionné le checkpoint.
model str | None toujours non l’id du checkpoint : Agent.model_id pour un Agent, l’alias résolu (par exemple "english") pour un Router.
agent Agent | ONNXAgent | None événements predict non le runtime qui répond à l’appel.
router Router | None événements Router non le Router, quand il y en a un en jeu.
max_len int | None toujours oui (start) budget de jetons par appel pour l’encodeur. None utilise la config de l’agent.
head_max_len int | None toujours oui (start) budget de jetons par appel pour la tête de question. None utilise la config de l’agent.
usage dict | None end oui (end) {"input_tokens", "output_tokens"}, additionnés sur les états de l’appel.
started_at float toujours non time.perf_counter() quand l’appel a commencé.
elapsed_ms float | None end non temps mural de tout l’appel, en millisecondes.
error BaseException | None chemin d’échec non l’exception, définie avant on_error et on_predict_end.

PredictContext.skip(results)

Court-circuite l’inférence. Appelé depuis on_predict_start, il définit ctx.results pour que la passe avant soit ignorée ; on_predict_end tourne quand même et les résultats fournis sont renvoyés.

La clé doit couvrir tout ce dont la réponse dépend, et le hook doit couvrir tout ce que l’appel transporte : les hooks se déclenchent une fois par appel, et predict_batch les appelle avec tous les états à la fois.

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)

tests/test_hooks_api.py exécute ce bloc, examples/hooks/cache.py, et les blocs de mise en cache de docs/hooks/patterns.md et docs/hooks/examples.md, et asserte les quatre clés de la même façon, donc une page ne peut pas enseigner une clé dont l’exemple a évolué.

Sur le Router, une charge utile ignorée se voit ajouter une clé routing (sans écraser une clé déjà présente), pour que Router.predict garde sa forme de retour documentée.

Protocole de hook

Hook est un typing.Protocol. Implémente n’importe quel sous-ensemble des méthodes ; les autres sont ignorées.

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: ...
événement où s’exécute peut changer
on_predict_start Agent, Router avant la tokenisation/la passe avant states, questions, ou skip()
on_predict_end Agent, Router une fois les résultats là, succès ou échec results
on_route Router après la détection, avant le chargement decision
on_load Router après la construction d’un checkpoint rien (observer)
on_evict Router après la libération d’un checkpoint rien (observer)
on_error Agent, Router quand un appel predict échoue rien (observer)

Un hook est libre de définir des attributs et méthodes supplémentaires ; seuls les six noms d’événement sont consultés. Si un hook définit l’un des six comme non appelable, la configuration échoue immédiatement (voir Validation).

BaseHook

BaseHook est la contrepartie concrète du protocole : une classe avec un corps sans effet pour chaque événement. Hérite-en et ne redéfinis que les événements dont tu as besoin.

from laya import BaseHook

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

Hook est préférable quand tu veux du typage structurel (n’importe quel objet avec les bonnes méthodes) ; BaseHook est préférable quand tu veux une base explicite à hériter et sur laquelle appeler super().

Types pratiques

PredictHook = Callable[[PredictContext], None]

PredictHook est le type d’un appelable simple utilisé avec on_predict_start= / on_predict_end=. Passe un seul appelable ou une séquence ; chacun est enveloppé dans un hook minimal.

Enregistrement à l’exécution

Chaque runtime hérite de HookRegistry, donc des hooks peuvent être ajoutés, retirés ou limités à un bloc après la construction. La mutation est sûre entre threads ; un appel lit un instantané de la liste, donc ajouter ou retirer un hook ne perturbe jamais un appel en cours.

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 accepte les mêmes objets que hooks= (pas les appelables simples). hooks_installed prend un nombre quelconque d’objets hook ou de séquences et restaure la liste précédente à la sortie, y compris quand le bloc lève une erreur.

Valeurs par défaut pour tout le processus

laya.hooks tient un petit registre à l’échelle du processus, pour qu’un traceur, un hook de métriques ou un tagueur de locataire n’ait pas à être passé à chaque Agent et Router. Les valeurs par défaut tournent en premier, puis les hooks installés sur l’instance, puis les hooks par appel.

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)

Les valeurs par défaut s’appliquent à chaque événement, y compris les événements de cycle de vie du Router on_load et on_evict. Le registre est lu au moment de l’appel, donc les hooks définis après la construction d’un Agent ou d’un Router s’appliquent quand même. Il n’y a pas d’opt-out par instance ; appelle clear_default_hooks() pour désactiver l’ensemble à l’échelle du processus.

Hooks asynchrones

Un événement peut être une coroutine. Enveloppe le hook dans AsyncHook et ses méthodes async def s’exécutent jusqu’au bout dans le cœur synchrone :

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())])

Un appelable asynchrone simple passé à on_predict_start= / on_predict_end= fonctionne aussi, parce que dispatch exécute tout awaitable que renvoie un hook.

Où tourne la coroutine :

  • Si le thread appelant n’a pas de boucle en cours, elle tourne avec asyncio.run.
  • S’il en a déjà une (un appelant dans une fonction async), elle tourne sur une boucle d’arrière-plan dédiée, pour que le thread appelant puisse bloquer sans interbloquer. Passe AsyncHook(hook, loop=...) pour la canaliser vers une boucle précise ; elle doit tourner, et ne pas être la propre boucle du thread appelant. Les deux sont vérifiés : une boucle arrêtée et la propre boucle de l’appelant lèvent chacune ValueError au lieu de bloquer pour toujours.

Un hook sans méthode async n’est pas affecté.

Surface de configuration

Chaque point d’entrée accepte les mêmes paramètres de hook. hooks prend un objet ou une séquence d’objets ; on_predict_start / on_predict_end prennent un appelable ou une séquence.

paramètre type défaut signification
hooks Hook | Sequence[Hook] | None None hooks de cycle de vie (n’importe lequel des six événements).
on_predict_start PredictHook | Sequence[PredictHook] | None None appelables de commodité pour un événement.
on_predict_end PredictHook | Sequence[PredictHook] | None None appelables de commodité pour un événement.
hooks_raise bool True True : une exception de hook se propage. False : avertir et continuer.
hooks_concurrent bool True False : dispatcher les hooks sous un verrou, un à la fois.
hooks_timeout float | None None limite de temps par hook en secondes ; None signifie aucune 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 et hooks_timeout sur une méthode par appel valent None par défaut, ce qui signifie « utilise la valeur de l’instance ».

  • hooks_concurrent est au niveau de l’instance uniquement.

  • Sur predict_long, les hooks enveloppent l’inférence qui répond à l’état, laquelle, pour un document ayant besoin de plusieurs fenêtres, est le predict_batch unique et partagé sur ces fenêtres : on_predict_start se déclenche une fois, et ctx.states contient les textes de fenêtre décodés dans l’ordre du balayage plutôt que l’état de l’appelant, qui a été tokenisé pour les produire. states est mutable depuis un hook de start, donc le balayage qui atteint l’inférence n’est pas forcément le découpage calculé par predict_long. Ce qui change, c’est ce que la réponse peut affirmer :

    ce qu’a fait le hook de start usage["windows"] answer["window"]
    a répondu avec ctx.skip([result]) 0 absent – aucune fenêtre ne l’a évalué
    a laissé le balayage tel que construit N présent – index, token_start/token_end nomment le span décisif
    a remplacé le balayage, de n’importe quelle façon les états qui ont été évalués absent – les offsets décrivent les fenêtres de predict_long, pas le texte qui a été évalué

    usage["windows"] est un total sur tous les chemins, y compris celui où un état tenait déjà dans une seule fenêtre, donc une réponse en cache ne se lit jamais comme une fenêtre que le modèle a lue.

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= par appel sur route, route_batch, predict et predict_batch s’applique à tout l’appel, y compris on_route. Sur predict_batch, la liste est composée comme sur predict (d’abord les hooks installés, puis la liste par appel. None et [] n’ajoutent rien) et s’exécute une fois par requête.
  • route() est public : l’appeler dispatche on_route avec les hooks installés plus tout hooks par appel.

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

ONNXAgent n’a pas de Router, donc il n’expose que les événements au niveau predict.

Charges utiles des événements

Quels champs sont remplis, par événement et par runtime :

événement runtime states questions decision model agent router results usage elapsed_ms error
on_predict_start Agent ✓ ✓ – ✓ ✓ – – – – –
on_predict_start Router ✓ ✓ ✓ ✓ ✓ ✓ – – – –
on_predict_end Agent ✓ ✓ – ✓ ✓ – ✓ au succès ✓ à l’échec
on_predict_end Router ✓ ✓ ✓ ✓ ✓ ✓ ✓ au succès ✓ à l’échec
on_error les deux ✓ ✓ ✓ (Router) ✓ ✓ ✓ (Router) – – – ✓
on_route Router ✓ ✓ ✓ – – ✓ – – – –
on_load Router [] {} – ✓ ✓ ✓ – – – –
on_evict Router [] {} – ✓ – ✓ – – – –

Détails de timing :

  • on_predict_end voit results sur le chemin du succès. Sur le chemin de l’échec, results est None sauf si un hook de start les a définis via skip(), et usage est donc None aussi (il est dérivé de results) ; elapsed_ms est toujours défini.
  • on_error tourne avant le bloc finally qui calcule elapsed_ms et usage, donc les deux sont None à ce moment-là. Lis le timing et l’usage depuis on_predict_end à la place.
  • run_id est toujours rempli.

Validation

La configuration est validée quand les hooks sont normalisés, ce qui a lieu à la construction pour les hooks installés et au moment de l’appel pour les hooks par appel. Ces cas lèvent TypeError :

cas message
une classe est passée au lieu d’une instance hooks entries must be instances, not classes; ...
un objet n’implémente aucun des six événements hooks entries must implement at least one of ...
un attribut d’événement n’est pas appelable hooks entry X.on_predict_start must be callable, got int
on_predict_start= / on_predict_end= n’est pas appelable on_predict_start must be callable, got int

hooks= n’accepte pas les appelables simples, parce qu’un appelable nu ne dit pas pour quel événement il est. Utilise on_predict_start= / on_predict_end= pour ceux-là.

Assistants avancés

Ceux-ci sont utilisés en interne et sont stables, mais la plupart des utilisateurs n’en ont pas besoin.

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 aplatit un objet/séquence hooks et les deux appelables en une seule liste ordonnée. dispatch appelle event sur chaque hook qui l’implémente, en appliquant la politique de levée, le verrou et le timeout, et exécute le résultat d’un hook s’il est awaitable. run_coroutine_sync exécute un awaitable jusqu’au bout depuis du code synchrone, sur la boucle de l’appelant si elle est libre, ou sur une boucle d’arrière-plan si l’appelant en a déjà une. aggregate_usage additionne les blocs d’usage par état.

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)

Voir aussi

  • Cycle de vie : quand chaque événement tourne, avec des organigrammes.
  • Erreurs : la matrice de défaillance et les règles d’enchaînement.
  • Motifs et anti-motifs : comment bien structurer les hooks.
  • Exemples : des recettes pour chaque cas d’usage.