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()eacomplete_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 esquema | complete_structured() |
| Entrada de imagem ou texto com um dicionário tipado de volta | complete_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; inspecioneresult.textpara 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ção | Padrão | Chave de configuração |
|---|---|---|
provider= | negado | allow_provider_override: true |
| ↳ lista permitida | — | allowed_providers: [...] |
model= | negado | allow_model_override: true |
| ↳ lista permitida | — | allowed_models: [...] |
agent_id= | negado | allow_agent_id_override: true |
profile= | negado | allow_profile_override: true |
task= embutida | negado | allow_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.entriespara 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.modelda 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çãoauxiliary.<task>.timeoutou ao padrão auxiliar global. - Formatação em JSON. Envia
response_formatao 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_schemaquando ojsonschemaestá 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.logcom o id do plugin, provedor/modelo, purpose e os totais de tokens.
O que é responsabilidade do plugin
- Formato da requisição.
messagespara chat,instructions+inputpara 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()levantaValueErrorem entradas vazias e em falha de validação de esquema.PluginLlmTrustErrordispara quando a barreira de confiança nega uma sobreposição. Qualquer outra coisa (5xx do provedor, nenhuma credencial configurada, tempo limite) levanta o que oauxiliary_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
- Implementação:
agent/plugin_llm.py - Testes:
tests/agent/test_plugin_llm.py - Plugins de referência (repositório complementar):
plugin-llm-example— extração estruturada síncrona com entrada de imagemplugin-llm-async-example— assíncrono comasyncio.gather()
- Cliente auxiliar (o motor por baixo): veja Runtime de provedores.