A Central de IA é um pequeno intermediário BYOK (traga sua própria chave) para o Moodle. Ela permite que os plugins da própria instituição gerem texto através de chaves de API de IA compartilhadas, sem que cada plugin reimplemente o transporte HTTP, o guard de SSRF, a escada de provedores ou um armazenamento de chaves.
Use a barra lateral para pular direto a qualquer seção desta página.
Código-fonte: github.com/jeanlucio/moodle-local_aihub
✨ Funcionalidades
- 🔑 Armazenamento BYOK: chaves de site (admin) e chaves pessoais opcionais (por usuário, opt-in) para Gemini, Groq, DeepSeek e qualquer endpoint compatível com OpenAI.
- 🪜 Resolução pessoal → site: o hub tenta a chave do próprio usuário primeiro, depois a de site, expondo um único resultado ao chamador.
- 🧩 Fachada de uma chamada:
\local_aihub\ai::generate_text()eis_available()— consumidas pelos plugins irmãos por dependência soft (class_exists), sem dependência dura. - 🚫 Não embrulha o
core_ai: cada consumidor mantém o próprio fallback paracore_ai, então um site que já temcore_aiconfigurado não precisa de nada extra — o hub continua opcional. - 👁️ Chaves pessoais write-only: uma vez salva, a chave pessoal nunca é devolvida ao navegador — a página mostra só o status configurada / não configurada, fechando a leitura via Entrar como.
- 🧑💻 Página self-service: Minhas chaves de IA (nas preferências do usuário) para adicionar/substituir/remover chaves pessoais e revisar o próprio uso recente.
- 📊 Relatório de uso para o admin: todas as requisições atendidas pelas chaves do site, de todos os usuários, com download CSV / Excel — protegido por
local/aihub:viewusage. - 🛡️ Guard SSRF: o endpoint compatível com OpenAI configurável é forçado a HTTPS, com bloqueio de loopback / link-local / faixas privadas e anti-rebinding de DNS A/AAAA.
- 🧾 Log de uso + task de retenção: uma linha por requisição (usuário, componente solicitante, o que foi gerado, provedor, modelo, tier da chave) e uma task agendada que limpa logs além de uma retenção configurável.
- 🔒 Privacidade completa: Privacy provider completo para o log de uso e as preferências pessoais, com os destinos externos declarados.
🧩 Como os consumidores usam
Um plugin irmão consome o hub através de um guard em runtime e mantém o próprio fallback para core_ai:
// Disponibilidade: chaves do hub primeiro, depois o fallback core_ai do consumidor.
public static function has_ai(): bool {
if (class_exists(\local_aihub\ai::class) && \local_aihub\ai::is_available()) {
return true; // BYOK via hub
}
return self::has_core_ai(); // funciona sem o hub
}
// Geração.
$result = \local_aihub\ai::generate_text(
'', // prompt de sistema (opcional)
$prompt, // prompt do usuário
false, // modo JSON
'your_frankenstyle', // 4º arg: componente chamador, para o log de uso
'Rótulo curto' // 5º arg: o que está sendo gerado, exibido no relatório de admin
);
if ($result['success']) {
// $result['data'] é texto CRU e não-confiável — valide e passe por format_text().
}
O texto devolvido é cru e não-confiável: valide sua estrutura e passe por format_text() antes de exibir ou persistir.
🔗 Resolução de chave (escada BYOK)
O hub resolve a chave tier por tier e para no primeiro tier que tiver uma chave:
| Tier | Fonte |
|---|---|
| 1 | Chave pessoal — a chave do próprio usuário, quando as chaves pessoais estão habilitadas e o usuário tem local/aihub:usepersonalkey |
| 2 | Chave de site — a chave de admin definida nas configurações do hub |
Dentro do tier escolhido, os provedores são tentados na ordem Gemini → Groq → DeepSeek → compatível com OpenAI (a primeira chave encontrada é usada; se a chamada falhar, o próximo provedor do mesmo tier é tentado). Quando nenhum tier tem chave, generate_text() retorna success = false — o hub nunca cai para o core_ai; essa decisão é do consumidor.
📦 Requisitos
| Componente | Versão |
|---|---|
| Moodle | 4.5+ |
| PHP | 8.1+ |
Nenhuma biblioteca de terceiros empacotada. Os provedores de IA são serviços externos, declarados no Privacy provider — não em thirdpartylibs.xml.
🛠️ Instalação e Configuração
- Baixe o arquivo
.zipou clone este repositório. - Extraia a pasta para o diretório
local/do seu Moodle. - Renomeie a pasta para
aihub(se necessário). Caminho final:seu-moodle/local/aihub/ - Acesse Administração do site → Notificações para concluir a instalação.
Configurar uma chave de provedor é opcional — o plugin instala e funciona sem nenhuma. Veja Como Usar logo abaixo para definir chaves de site e habilitar chaves pessoais.
📖 Como Usar
- Configurar chaves de site (admin): Administração do site → Plugins → Plugins locais → Central de IA → Configurações. Defina qualquer uma das chaves Gemini, Groq, DeepSeek ou compatível com OpenAI e, opcionalmente, habilite as chaves de API pessoais.
- Adicionar uma chave pessoal (usuário): usuários com a capability
local/aihub:usepersonalkeyganham uma entrada Minhas chaves de IA nas preferências, onde guardam a própria chave (write-only) e veem o uso recente. - Revisar o uso das chaves de site (admin): o Relatório de uso de IA do site (sob a categoria Central de IA, capability
local/aihub:viewusage) lista todas as requisições atendidas pelas chaves do site, de todos os usuários, com download CSV e Excel. - Consumir de um plugin (desenvolvedor): chame
\local_aihub\ai::generate_text()atrás de um guardclass_exists(), mantendo o próprio fallback paracore_ai(ver Como os consumidores usam).
🧪 Testes Automatizados
O hub vem com uma suíte PHPUnit e Behat; todo push de CI roda contra a matriz (Moodle 4.5 → 5.x, PostgreSQL & MariaDB).
PHPUnit — Testes Unitários e de Integração
| Arquivo de teste | Casos | O que cobre |
|---|---|---|
tests/local/keys_test.php |
6 | Defaults de URL/modelo OpenAI; roundtrip get/save/clear da chave pessoal; roundtrip da URL/modelo pessoal compatível com OpenAI; personal_keys_allowed respeitando o toggle e a capability; resolução pessoal → site; has_any_key entre chaves pessoal/site |
tests/local/client_test.php |
9 | Casos de SSRF do is_safe_url (http, loopback, faixa privada, IP público); branch de DNS rebinding simulado via dns_stub_client — IP resolvido privado é bloqueado, IP resolvido público passa, sem registros passa; resolve_openai_url anexa /chat/completions; tier pessoal vence o de site; fall-through de provedor dentro de um tier (Gemini → Groq, e Gemini/Groq → DeepSeek); sem chave → success=false (sem HTTP real) |
tests/local/usage_log_test.php |
3 | Inserção do registro (com keysource, modelo vazio nulado); leitores por chave de site excluindo linhas pessoais/sem tag; filtragem recente por usuário |
tests/local/export_test.php |
2 | Export do uso pessoal (todas as linhas, todas as colunas); export do relatório de chaves de site incluindo a coluna de usuário e excluindo o uso pessoal |
tests/ai_test.php |
4 | Estados de is_available; uma geração bem-sucedida registra o componente chamador, a descrição e o tier da chave; uma geração com falha não registra nada; report_usage() grava uma linha de log para um consumidor que resolveu a própria chave |
tests/privacy_provider_test.php |
4 | Declaração de metadata; descoberta de contexto/usuário; export_user_data (linhas do log + valor da chave redigido); deleção por usuário com isolamento |
tests/output/mykeys_test.php |
2 | Status de chave pessoal por provedor (definida/não definida), sem nunca colocar o valor da chave no contexto do template; linhas de uso recente com o ícone de provedor correto, incluindo o fallback pra provedor não reconhecido |
tests/output/report_test.php |
2 | Linhas do relatório de chaves de site com o nome do usuário solicitante e o ícone de provedor correto, excluindo linhas de chave pessoal; estado vazio quando não há uso de chaves de site |
tests/task/purge_old_logs_test.php |
2 | Linhas mais antigas que a retenção configurada são apagadas, linhas mais recentes são mantidas; retenção 0 mantém todas as linhas indefinidamente |
| Total | 34 |
vendor/bin/phpunit --testsuite local_aihub
Cobertura de linhas por classe (PHPUnit + Xdebug):
| Classe | Cobertura de linhas |
|---|---|
ai |
85% |
local\client |
36% |
local\export |
83% |
local\keys |
92% |
local\usage_log |
85% |
output\mykeys |
100% |
output\renderer |
0% |
output\report |
100% |
privacy\provider |
86% |
task\purge_old_logs |
86% |
| Total | 73% |
Duas classes não são testadas por unidade por completo, ambas pelo mesmo motivo — as linhas não cobertas são chamadas de rede reais, não lógica de negócio:
local\client:call_gemini(),call_groq(),call_deepseek(),call_openai_compatible()ehttp_post()fazem chamadas HTTP reais a provedores externos, então nunca são testadas por unidade contra a rede real. Otests/fixtures/mock_client.phpsobrescreve esses métodos pra testar a escada de resolução de chave isoladamente; os próprios métodos de transporte são verificados manualmente contra as APIs reais antes de cada release. O branch de DNS rebinding dois_safe_url()caía na mesma categoria (uma chamada real adns_get_record()), mas a etapa de resolução foi extraída praresolve_dns(), que otests/fixtures/dns_stub_client.phpsobrescreve — então esse branch agora está totalmente coberto sem nenhuma consulta DNS real.output\renderer: seu único método delega uma única linha prarender_from_template(). Verificar o HTML resultante é papel da suíte Behat abaixo (mykeys.feature), não do PHPUnit.
Behat — Testes de Aceitação
| Arquivo de feature | Cenários | O que cobre |
|---|---|---|
tests/behat/mykeys.feature |
2 | Um provedor começa não configurado; salvar uma chave pessoal marca-a como configurada sem revelar o valor guardado |
| Total | 2 |
php admin/tool/behat/cli/init.php
vendor/bin/behat --tags=@local_aihub --profile=chrome
🔐 Segurança e Conformidade
- Chaves pessoais protegidas por capability e opt-in (
local/aihub:usepersonalkeye o toggle de site). - Chaves pessoais write-only — nunca devolvidas à página após salvas, mesmo sob Entrar como.
- Chaves de site guardadas com
admin_setting_configpasswordunmask; o guard SSRF protege o endpoint configurável. require_sesskey()no formulário de chaves; capabilitylocal/aihub:viewusageno relatório de uso.- Cobertura completa da Privacy API (export/delete do log de uso e das preferências pessoais) com os destinos externos declarados.
🔎 Divulgação de Serviço de Terceiros
O hub transmite o texto do prompt a um provedor de IA de terceiros apenas quando uma chave está configurada e uma geração é solicitada.
Uma chave de API é obrigatória?
Não. O plugin instala e funciona sem nenhuma chave — apenas informa que não há fonte disponível, e os consumidores caem para a própria integração com core_ai. Nenhuma requisição externa é feita até que uma chave de site ou pessoal seja definida.
Provedores suportados
- Google Gemini — https://ai.google.dev/ — modelo:
gemini-flash-latest(alias rolante, sempre a versão Flash atual do Google; fixo, não configurável) - Groq — https://console.groq.com/ — modelo:
openai/gpt-oss-120b(fixo, não configurável; ver Escolha do modelo Groq abaixo) - DeepSeek — https://deepseek.com/ — modelo:
deepseek-v4-flash(fixo, não configurável) - APIs compatíveis com OpenAI — qualquer provedor que siga o formato da API OpenAI (OpenRouter, modelos auto-hospedados via LM Studio, um proxy Ollama, etc.) — modelo: configurável por chave (config de site / chave pessoal), padrão
gpt-4o-miniquando vazio
Esses serviços operam sob seus próprios termos de serviço e políticas de privacidade.
Escolha do modelo Groq
O slot da Groq chama um único modelo fixo, openai/gpt-oss-120b. A Groq descontinua periodicamente
IDs de modelo específicos (ex.: llama-3.3-70b-versatile, descomissionado em 16/08/2026) e espera
migração para um substituto nomeado. Entre as opções gratuitas recomendadas pela Groq para essa
migração, o openai/gpt-oss-120b foi escolhido em vez do qwen/qwen3.6-27b por seguir instruções
melhor e ser mais confiável em modo JSON entre os prompts variados enviados pelos diferentes
plugins consumidores.
Como obter uma chave de API
As chaves de API são criadas diretamente no site oficial do provedor. Gemini, Groq e DeepSeek atualmente oferecem camadas de uso gratuitas ou créditos de teste (as políticas de preço podem mudar). O hub não fornece chaves de API.
Onde as chaves são configuradas
- Chave pessoal — definida por cada usuário em Minhas chaves de IA (preferências), quando as chaves pessoais estão habilitadas e o usuário tem a capability.
- Chave de site — definida pelo admin em Administração do site → Plugins → Plugins locais → Central de IA.
Transmissão de Dados
Quando uma chave é resolvida para um provedor, o texto do prompt é transmitido à API desse provedor para gerar a resposta:
- Google Gemini —
generativelanguage.googleapis.com - Groq —
api.groq.com - DeepSeek —
api.deepseek.com - Compatível com OpenAI — o endpoint configurado pelo admin ou usuário (padrão
api.openai.com)
O hub guarda um log de uso (quem solicitou, qual componente, um rótulo curto do que foi gerado, provedor, modelo, tier da chave e horário), mas não guarda prompts nem respostas da IA. Todos os destinos externos estão declarados no Privacy provider do plugin.
Credenciais de demonstração
Não aplicável — nenhuma credencial é exigida para instalar ou usar o hub. Todo recurso de IA fica inerte até que uma chave de site ou pessoal seja configurada.
📄 Licença
Este projeto está licenciado sob a GNU General Public License v3 (GPLv3).
Copyright: 2026 Jean Lúcio