Arquitetura¶
O Adaptive Learner é uma aplicação orientada a plugins com 4 camadas.
┌─────────────────────────────────────────────────────────────┐
│ Frontend React 19 + TypeScript 6 + Vite 8 + │
│ Vitest 4 + Dexie 4 (IndexedDB) + TipTap │
└─────────────────────────────────────────────────────────────┘
↑↓ /api/*
┌─────────────────────────────────────────────────────────────┐
│ Backend FastAPI ^0.136 + SQLAlchemy ^2.0 + │
│ Pydantic v2 + Alembic + Fernet │
└─────────────────────────────────────────────────────────────┘
↑↓ hookspecs
┌─────────────────────────────────────────────────────────────┐
│ PluginForge ^0.10.0 (PyPI externo; com restrição de │
│ identidade via target_application) │
└─────────────────────────────────────────────────────────────┘
↑↓ entry_points
┌─────────────────────────────────────────────────────────────┐
│ Plugins 10 pacotes em plugins/ │
│ (ai-{anthropic,openai,gemini}, assessment,│
│ session, tracking, tools, gamification, │
│ anki, notebooklm) │
└─────────────────────────────────────────────────────────────┘
Novas funcionalidades pertencem SEMPRE a um plugin, exceto se tocarem no núcleo (utilizadores / projetos / definições / currículo / tópicos / lições / backup / sincronização / sistema / importação).
Armazenamento duplo (v0.7.0)¶
O frontend tem uma única costura onde o armazenamento é escolhido:
getStorage(): IStorageService. Duas implementações satisfazem um
contrato:
apiStorage(padrão): invólucro fino em torno deapi/client.tsque comunica com o backend FastAPI.dexieStorage(local primeiro): pilha IndexedDB completa espelhando todos os 25 modelos SQLAlchemy. As chamadas de IA disparam diretamente do navegador viastorage/ai-providers.ts.
IStorageService expõe 22 espaços de nomes (users, projects,
settings, assessment, session com streaming, tracking, tools,
curricula, topics, lessons, plugins, system, backup, export,
subjects, tags, projectTaxonomy, imports, gamification, anki,
pronunciation, notebooklm). Ambos os backends implementam cada
método.
A fábrica lê localStorage["adaptive-learner.storage_mode"], depois
VITE_STORAGE_MODE (definido pela compilação para GH Pages), e
depois usa api como padrão. Mudar de modo não é uma troca em
tempo real: a página de Definições persiste a escolha e notifica
com um toast de recarregamento necessário.
Segredos em três camadas (v1.20.0 / Fase 34)¶
variáveis de ambiente > secrets.yaml > coluna DB Fernet
ADAPTIVE_LEARNER_* ~/.config/...yaml api_key_<provider>
Cada chamada de IA percorre a cadeia via
services/settings.resolve_api_key:
- Variável de ambiente
ADAPTIVE_LEARNER_<PROVIDER>_API_KEY. ai.<provider>.api_keyem~/.config/adaptive_learner/secrets.yaml.- Coluna DB desencriptada com Fernet.
None— a chamada de IA apresenta um erro na interface.
A atribuição de fonte vive em UserSettingsOut.key_source_*
(enum: env / secrets_yaml / settings / none).
A interface de Definições desativa Guardar / Remover quando a
fonte é env ou secrets_yaml.
A mesma cadeia aplica-se às substituições default_model por
fornecedor; secrets.yaml ganha sobre a substituição da interface
por design da Fase 34 (configuração em ficheiro ganha sobre a
interface para utilizadores avançados).
Estrutura de plugins¶
plugins/adaptive-learner-plugin-<name>/
adaptive_learner_<name>/
plugin.py # <Name>Plugin(BasePlugin), implementações de hooks
routes.py # roteador FastAPI (delega para funções de serviço)
<module>.py # lógica de negócio
tests/
test_*.py # testes pytest
pyproject.toml # ponto de entrada: [project.entry-points."adaptive_learner.plugins"]
- A classe Plugin herda de
BasePlugin(pluginforge). - A lógica de negócio vive nos seus próprios módulos, NÃO em routes.py.
- routes.py contém apenas endpoints FastAPI que delegam.
- As especificações de hooks vivem em
backend/app/hookspecs.py. - Dependências do plugin como atributo de classe:
depends_on = ["session"]. - Todos os plugins são gratuitos (MIT). A infraestrutura de
licenciamento existe mas está dormente (
LICENSING_ENABLED = False).
Hooks (8 especificações em backend/app/hookspecs.py)¶
| Hook | Quando | Primeiro resultado? |
|---|---|---|
get_assessment_questions(lang) |
Carregamento da página de Avaliação | sim |
calculate_profile(answers) |
Submissão da Avaliação | sim |
create_session_prompt(...) |
Cada turno de chat | sim |
ai_complete(messages, model, api_key, max_tokens) |
Chamada de IA padrão | sim (rotas por prefixo de modelo) |
ai_complete_async(...) |
Avaliação paralela em limite de ciclo (v1.5.0) | sim |
ai_complete_stream(...) |
Resposta de sessão em streaming (v1.6.0) | sim |
recommend_method_switch(...) |
Dashboard + Sessão | sim |
on_session_complete(session, rating) |
Fim de sessão | broadcast |
get_progress_summary(project_id) |
Widgets do Dashboard | broadcast |
get_tool_recommendations(profile, lang) |
Ferramentas do Dashboard | broadcast |
Fluxo de dados¶
Interface (React) → IStorageService
→ (modo API) roteador FastAPI → serviço → SQLAlchemy → SQLite
→ (modo Dexie) tabela Dexie → IndexedDB
↓
Orquestrador de IA → resolve_api_key (env > yaml > BD)
→ pluginforge → plugin do fornecedor ai_complete*
→ SDK Anthropic / OpenAI / Gemini
Unidirecional. Sem acesso direto à BD a partir dos roteadores (os serviços possuem o trabalho SQLAlchemy). Sem código frontend no backend.
Tratamento de erros¶
Frontend ApiError (status + detail) → toast para o utilizador
Cliente API Erro HTTP → convertido em ApiError
Roteador Fino, não captura nada. O gestor global de exceções mapeia.
Serviço Lança subclasses de AdaptiveLearnerError
Plugin Lança PluginError(plugin_name, message)
Externo ExternalServiceError(service, message) para SDKs de fornecedores
Os serviços NUNCA lançam HTTPException; os roteadores não capturam
NADA. O gestor global de exceções em main.py mapeia erros de
domínio para códigos de estado HTTP. Consulte
.claude/rules/code-hygiene.md para o padrão completo.
Persistência¶
- Backend: SQLAlchemy + SQLite. Migrações Alembic em
backend/migrations/versions/. - Superfície de sincronização: 28 tabelas (linha de base v1.19.0). Linhas de histórico somente-adição (sessões, mensagens, classificações, commits de progresso, avaliações de passo, mudanças de método, conversas importadas, mensagens importadas, cartões anki, questões de estudo) mais definições mutáveis + linhas de currículo.
- Formato de backup: JSON; chaves de API removidas na exportação; a restauração é uma fusão.
- Isolamento de testes: os diretórios de dados de produção têm um
marcador
.adaptive-learner-production; se um teste o vir, a execução aborta compytest.exit(returncode=2).
Temas¶
5 temas (Classic, Cool Modern, Nord, Notebook, Studio) ×
claro/escuro = 10 variantes. Variáveis CSS em todo o lado; sem
Tailwind. Propriedades personalizadas em
frontend/src/styles/global.css. Novos elementos de interface
DEVEM usar o conjunto de variáveis.
Mobile / PWA¶
@media (max-width: 768px) é o ponto de corte canonical para
mobile (gaveta de hamburger, alvos de toque de 44×44, layouts
empilhados). @media (max-width: 360px) é a rede de segurança
para ecrãs muito estreitos. Estilos de desktop ≥769px sem
alterações.
Service worker (Workbox via vite-plugin-pwa): NetworkFirst em
GET /api/ com timeout de 4s, LRU de 24h, limite de 60 entradas.
/api/ mutante é NetworkOnly.