Pular para o conteúdo principal

Documentação oficial desta página · v0.20.6

Acesso a LLM em plugins

O ctx.llm é o jeito suportado de um plugin fazer uma chamada de LLM. Chat completion, extração estruturada, síncrono, assíncrono, com ou sem imagens — mesma superfície, mesma barreira de confiança, mesmas credenciais sob controle do host.

Os plugins recorrem a isso quando precisam fazer algo que envolve o modelo mas não faz parte da conversa do agente. Um hook que reescreve o erro de uma ferramenta em algo que quem não é engenheiro consiga ler. Um adaptador de gateway que traduz uma mensagem recebida antes de colocá-la na fila. Um comando de barra que resume um texto colado muito longo. Uma tarefa agendada que avalia a atividade de ontem e escreve uma linha em um quadro de status. Um pré-filtro que decide se uma mensagem merece acordar o agente.

São trabalhos em que o agente não deveria estar no meio. Eles querem uma chamada de LLM, uma resposta tipada, e acabou.

A menor chamada possível​

result = ctx.llm.complete(messages=[{"role": "user", "content": "ping"}])
return result.text

Essa é a API inteira em uma linha. Sem chaves, sem configuração de provedor, sem inicialização de SDK. O plugin roda contra o provedor e o modelo que o usuário estiver usando no momento — quando ele troca de provedor, o plugin o acompanha automaticamente.

Um exemplo de chat mais completo​

result = ctx.llm.complete(
messages=[
{"role": "system", "content": "Rewrite errors as one short sentence a non-engineer can act on."},
{"role": "user", "content": traceback_text},
],
max_tokens=64,
purpose="hooks.error-rewrite",
)
return result.text

purpose é uma string livre de auditoria — ela aparece no agent.log e em result.audit, para que os operadores vejam qual plugin fez qual chamada. É opcional, mas recomendada para qualquer coisa que dispare com frequência.

Saída estruturada​

Quando o plugin precisa de uma resposta tipada, mude para a via estruturada:

result = ctx.llm.complete_structured(
instructions="Score this support reply for urgency (0–1) and pick a category.",
input=[{"type": "text", "text": message_body}],
json_schema=TRIAGE_SCHEMA,
purpose="support.triage",
temperature=0.0,
max_tokens=128,
)

if result.parsed["urgency"] > 0.8:
await dispatch_to_oncall(result.parsed["category"], message_body)

O host pede a saída em JSON ao provedor, faz o parsing localmente como alternativa, valida contra o seu esquema se o jsonschema estiver instalado e devolve um objeto Python em result.parsed. Se o modelo não conseguiu produzir JSON válido, result.parsed é None e result.text carrega a resposta bruta.

O que essa via te dá​

  • Uma chamada, quatro formatos. complete() para chat, complete_structured() para JSON tipado, acomplete() e acomplete_structured() para asyncio. Mesmos argumentos, mesmos objetos de resultado.
  • Credenciais sob controle do host. Tokens OAuth, fluxos de renovação, o pool de credenciais, sobreposições auxiliares por tarefa — todo conceito de credencial que o Hermes já tem se aplica. O plugin nunca vê um token; o host atribui a chamada de volta por meio de result.audit.
  • Delimitada. Uma única chamada síncrona ou assíncrona. Sem streaming, sem laços de ferramenta, sem estado de conversa para gerenciar. Diga a entrada, receba o resultado, retorne.
  • Confiança que falha fechada. Um plugin que você nunca configurou não pode escolher o próprio provedor, modelo, agente ou credencial guardada. A postura padrão é "use o que o usuário está usando". Os operadores liberam sobreposições específicas, plugin a plugin, no config.yaml.

Começo rápido​

Dois plugins completos abaixo — um de chat, outro estruturado. Os dois cabem dentro de uma única função register(ctx) e não precisam de nenhuma configuração externa para rodar contra o modelo que o usuário tiver ativo.

Chat completion — /tldr​

def register(ctx):
ctx.register_command(
name="tldr",
handler=lambda raw: _tldr(ctx, raw),
description="Summarise the supplied text in one paragraph.",
args_hint="<text>",
)


def _tldr(ctx, raw_args: str) -> str:
text = raw_args.strip()
if not text:
return "Usage: /tldr <text to summarise>"
result = ctx.llm.complete(
messages=[
{"role": "system",
"content": "Summarise the user's text in one tight paragraph. No preamble."},
{"role": "user", "content": text},
],
max_tokens=256,
temperature=0.3,
purpose="tldr",
)
return result.text

result.text é a resposta do modelo; result.usage traz as contagens de tokens; result.provider e result.model trazem a atribuição.

Extração estruturada — /paste-to-tasks​

def register(ctx):
ctx.register_command(
name="paste-to-tasks",
handler=lambda raw: _paste_to_tasks(ctx, raw),
description="Turn freeform meeting notes into structured tasks.",
args_hint="<text>",
)


_TASKS_SCHEMA = {
"type": "object",
"properties": {
"tasks": {
"type": "array",
"items": {
"type": "object",
"properties": {
"owner": {"type": "string"},
"action": {"type": "string"},
"due": {"type": "string", "description": "ISO date or empty"},
},
"required": ["action"],
},
},
},
"required": ["tasks"],
}


def _paste_to_tasks(ctx, raw_args: str) -> str:
if not raw_args.strip():
return "Usage: /paste-to-tasks <meeting notes>"
result = ctx.llm.complete_structured(
instructions=(
"Extract concrete action items from these meeting notes. "
"One task per actionable line. If no owner is named, leave 'owner' blank."
),
input=[{"type": "text", "text": raw_args}],
json_schema=_TASKS_SCHEMA,
schema_name="meeting.tasks",
purpose="paste-to-tasks",
temperature=0.0,
max_tokens=512,
)
if result.parsed is None:
return f"Couldn't parse a response. Raw output:\n{result.text}"
lines = [f"- [{t.get('owner') or '?'}] {t['action']}" for t in result.parsed["tasks"]]
return "\n".join(lines) or "(no tasks found)"

Um terceiro exemplo trabalhado, dessa vez com entrada de imagem, está no repositório hermes-example-plugins (repositório complementar de plugins de referência — não vem junto com o hermes-agent). Para a superfície assíncrona (acomplete() / acomplete_structured() com asyncio.gather()), veja o plugin-llm-async-example no mesmo repositório.

Quando usar cada um​

Você quer…Use
Uma resposta em texto livre (tradução, resumo, reescrita, geração)complete()
Um prompt de vários turnos (sistema + exemplos few-shot + usuário)complete()
Um dicionário tipado de volta, validado contra um esquemacomplete_structured()
Entrada de imagem ou texto com um dicionário tipado de voltacomplete_structured()
A mesma chamada a partir de código assíncrono (adaptadores de gateway, hooks assíncronos)acomplete() / acomplete_structured()

Todo o resto — seleção de provedor, resolução de modelo, autenticação, alternativa em caso de falha, tempo limite, roteamento de visão — é igual nos quatro.

Superfície da API​

ctx.llm é uma instância de agent.plugin_llm.PluginLlm.

complete()​

result = ctx.llm.complete(
messages=[{"role": "user", "content": "Hi"}],
provider=None, # opcional, sujeito à barreira — id de provedor do Hermes (ex.: "openrouter")
model=None, # opcional, sujeito à barreira — qualquer string que esse provedor espere
temperature=None,
max_tokens=None,
timeout=None, # segundos
agent_id=None, # opcional, sujeito à barreira
profile=None, # opcional, sujeito à barreira — nome explícito do perfil de autenticação
purpose="optional-audit-string",
task=None, # opcional — um slot auxiliar registrado pelo plugin
)
# → PluginLlmCompleteResult(text, provider, model, agent_id, usage, audit)

Chat completion simples. messages segue o formato padrão da OpenAI — uma lista de dicionários {"role": "...", "content": "..."}. Prompts de vários turnos (sistema + pares few-shot de usuário/assistente + usuário final) funcionam exatamente como funcionariam com o SDK da OpenAI.

provider= e model= são independentes e seguem o mesmo formato da configuração principal do host (model.provider + model.model). Defina só model= para usar o provedor ativo do usuário com outro modelo nele. Defina os dois para trocar de provedor por completo. Qualquer um dos argumentos sem adesão do operador levanta PluginLlmTrustError.

complete_structured()​

result = ctx.llm.complete_structured(
instructions="What you want extracted.",
input=[
{"type": "text", "text": "..."},
{"type": "image", "data": b"...", "mime_type": "image/png"},
{"type": "image", "url": "https://..."},
],
json_schema={...}, # opcional — aciona resultado com parsing + validação
json_mode=False, # defina True sem esquema para pedir JSON mesmo assim
schema_name=None, # opcional — nome legível do esquema
system_prompt=None,
provider=None, # opcional, sujeito à barreira
model=None, # opcional, sujeito à barreira
temperature=None,
max_tokens=None,
timeout=None,
agent_id=None,
profile=None,
purpose=None,
task=None, # opcional — um slot auxiliar registrado pelo plugin
)
# → PluginLlmStructuredResult(text, provider, model, agent_id,
# usage, parsed, content_type, audit)

As entradas são blocos tipados de texto ou de imagem (bytes crus são codificados em base64 como uma URL data: automaticamente). Quando json_schema ou json_mode=True é informado, o host pede a saída em JSON via response_format, faz o parsing localmente como alternativa e valida contra o seu esquema se o jsonschema estiver instalado.

  • result.content_type == "json" — result.parsed é um objeto Python que corresponde ao seu esquema.
  • result.content_type == "text" — o parsing ou a validação falhou; inspecione result.text para ver a resposta bruta do modelo.

Assíncrono​

result = await ctx.llm.acomplete(messages=..., task="classifier")
result = await ctx.llm.acomplete_structured(
instructions=..., input=..., task="classifier"
)

Mesmos argumentos e mesmos tipos de resultado das versões síncronas. Use essas funções em adaptadores de gateway, hooks assíncronos ou em qualquer código de plugin que já rode em um laço do asyncio.

Chamadas auxiliares roteadas por tarefa​

Passe task= em qualquer um dos quatro formatos de chamada quando um plugin precisar da própria rota auxiliar configurada. Registre essa tarefa na configuração do plugin; os padrões do plugin valem até o operador sobrepor o provedor e o modelo dela em auxiliary.<task>:

def register(ctx):
ctx.register_auxiliary_task(
"classifier", display_name="Classifier", description="Classify input."
)


result = ctx.llm.complete(messages=[...], task="classifier")
result = ctx.llm.complete_structured(instructions=..., input=..., task="classifier")
auxiliary:
classifier:
provider: openrouter
model: vendor/model-id

Os plugins podem fornecer padrões de registro de provedor/modelo para as próprias tarefas. A configuração do operador em auxiliary.<task> sobrepõe esses padrões e controla a escolha da implantação. Um plugin só pode usar uma tarefa que ele mesmo registrou; nomes de tarefa desconhecidos ou de terceiros falham antes de o provedor ser acionado. allow_task_override: true é uma concessão explícita do operador para usar tarefas auxiliares embutidas do Hermes; ela não libera as tarefas de outro plugin. Omita task= (ou use "auto") para manter o provedor/modelo principal ativo.

Atributos do resultado​

@dataclass
class PluginLlmCompleteResult:
text: str # a resposta do assistente
provider: str # ex.: "openrouter", "anthropic"
model: str # o que o provedor devolveu para esta chamada
agent_id: str # de quem foram o modelo e a autenticação usados
usage: PluginLlmUsage # tokens + cache + estimativa de custo
audit: Dict[str, Any] # plugin_id, purpose, profile

@dataclass
class PluginLlmStructuredResult:
# mesmos campos de PluginLlmCompleteResult, mais:
parsed: Optional[Any] # objeto JSON quando content_type == "json"
content_type: str # "json" ou "text"
# audit também carrega schema_name quando informado

usage traz input_tokens, output_tokens, total_tokens, cache_read_tokens, cache_write_tokens e cost_usd quando o provedor devolve esses campos.

Barreira de confiança​

O comportamento padrão é falhar fechado. Sem um bloco de configuração plugins.entries, um plugin pode:

  • rodar qualquer um dos quatro métodos contra o provedor e o modelo ativos do usuário,
  • definir argumentos que moldam a requisição (temperature, max_tokens, timeout, system_prompt, purpose, messages, instructions, input, json_schema),

…e é só. Os argumentos provider=, model=, agent_id= e profile= levantam PluginLlmTrustError até o operador liberar. Da mesma forma, task= só pode usar a tarefa auxiliar registrada pelo próprio plugin, a não ser que o operador conceda allow_task_override para uma tarefa embutida.

A maioria dos plugins nunca precisa desta seção. Um plugin que apenas chama ctx.llm.complete(messages=...) sem sobreposições roda contra o que o usuário tiver ativo e funciona sem configuração nenhuma. O bloco abaixo só importa quando um plugin quer especificamente fixar um modelo ou um provedor diferente do usuário.

plugins:
entries:
my-plugin:
llm:
# Permitir que este plugin escolha outro provedor do Hermes
# (precisa ser um que o Hermes já conheça — os mesmos nomes do
# `hermes model` e do model.provider no config.yaml).
allow_provider_override: true

# Opcionalmente, restrinja quais provedores. Use ["*"] para qualquer um.
allowed_providers:
- openrouter
- anthropic

# Permitir que este plugin peça um modelo específico.
allow_model_override: true

# Opcionalmente, restrinja quais modelos. Use ["*"] para qualquer um.
# Os modelos são comparados literalmente com a string que o plugin
# enviar — o Hermes não consulta nada.
allowed_models:
- openai/gpt-4o-mini
- anthropic/claude-3-5-haiku

# Permitir chamadas entre agentes (raro).
allow_agent_id_override: false

# Permitir que o plugin peça um perfil de autenticação guardado
# específico (por exemplo, outra conta OAuth no mesmo provedor).
allow_profile_override: false

O id do plugin é o campo name: do manifesto, no caso de plugins simples, ou a chave derivada do caminho, no caso de plugins aninhados (image_gen/openai, memory/honcho, etc.).

O que a barreira impõe​

SobreposiçãoPadrãoChave de configuração
provider=negadoallow_provider_override: true
↳ lista permitida—allowed_providers: [...]
model=negadoallow_model_override: true
↳ lista permitida—allowed_models: [...]
agent_id=negadoallow_agent_id_override: true
profile=negadoallow_profile_override: true
task= embutidanegadoallow_task_override: true

Cada sobreposição é liberada de forma independente. Conceder allow_model_override não concede também allow_provider_override — um plugin em quem se confia para escolher o modelo continua preso ao provedor ativo do usuário, a menos que receba também a liberação de provedor.

O que a barreira NÃO precisa impor​

  • Argumentos que moldam a requisição — temperature, max_tokens, timeout, system_prompt, purpose, messages, instructions, input, json_schema, schema_name, json_mode — são sempre permitidos; eles não escolhem credenciais nem rotas.
  • A postura padrão de negar significa que um plugin não configurado ainda consegue fazer um trabalho útil — ele só roda contra o provedor e o modelo ativos. Os operadores só precisam pensar em plugins.entries para plugins que querem um roteamento mais fino.

O que é responsabilidade do host​

Uma lista completa das coisas que o ctx.llm faz pelo plugin, para você não precisar fazer:

  • Resolução de provedor. Lê model.provider + model.model da configuração do usuário (ou as sobreposições explícitas, quando confiáveis).
  • Autenticação. Puxa chaves de API, tokens OAuth ou tokens de renovação de ~/.hermes/auth.json / do ambiente, incluindo o pool de credenciais quando há um configurado. O plugin nunca os vê.
  • Roteamento de visão. Quando há entrada de imagem e o modelo de texto ativo do usuário só aceita texto, o host recorre automaticamente ao modelo de visão configurado.
  • Cadeia de alternativas. Se o provedor principal do usuário responder 5xx ou 429, a requisição passa pela alternativa habitual do Hermes, ciente de agregadores, antes de devolver um erro ao plugin.
  • Tempo limite. Respeita o seu argumento timeout=, recorrendo à configuração auxiliary.<task>.timeout ou ao padrão auxiliar global.
  • Formatação em JSON. Envia response_format ao provedor quando você pede JSON e depois refaz o parsing localmente a partir de uma resposta cercada por marcação de código, se o provedor devolveu uma.
  • Validação de esquema. Valida contra o seu json_schema quando o jsonschema está instalado; caso contrário, registra uma linha de debug e pula a validação estrita.
  • Log de auditoria. Cada chamada escreve uma linha INFO no agent.log com o id do plugin, provedor/modelo, purpose e os totais de tokens.

O que é responsabilidade do plugin​

  • Formato da requisição. messages para chat, instructions + input para estruturado. O plugin monta o prompt; o host executa.
  • Esquema. O formato que você quiser de volta. O host não deduz isso para você.
  • Tratamento de erros. complete_structured() levanta ValueError em entradas vazias e em falha de validação de esquema. PluginLlmTrustError dispara quando a barreira de confiança nega uma sobreposição. Qualquer outra coisa (5xx do provedor, nenhuma credencial configurada, tempo limite) levanta o que o auxiliary_client.call_llm() levantar.
  • Custo. Toda chamada roda contra o provedor pago do usuário. Não fique chamando complete() em laço para cada mensagem do gateway sem pensar no gasto em tokens.

Onde isso se encaixa na superfície de plugins​

Os métodos ctx.* existentes estendem algum subsistema do Hermes que já existe:

| ctx.register_tool | adiciona uma ferramenta que o agente pode chamar | | ctx.register_platform | liga um novo adaptador de gateway | | ctx.register_image_gen_provider | substitui o backend de geração de imagem | | ctx.register_memory_provider | substitui o backend de memória | | ctx.register_context_engine | substitui o compressor de contexto | | ctx.register_hook | observa um evento do ciclo de vida |

O ctx.llm é a primeira superfície que permite a um plugin rodar o mesmo modelo com que o usuário está conversando, fora da conversa, sem nada do que está acima. Esse é o único trabalho dele. Se o seu plugin precisa registrar uma ferramenta que o agente invoca, use register_tool. Se ele precisa reagir a um evento de ciclo de vida, use register_hook. Se ele precisa fazer a própria chamada de modelo — por qualquer motivo, estruturada ou não —, ctx.llm.

Referência​