アーキテクチャ¶
Adaptive Learnerは4層のプラグイン駆動型アプリケーションです。
┌─────────────────────────────────────────────────────────────┐
│ フロントエンド React 19 + TypeScript 6 + Vite 8 + │
│ Vitest 4 + Dexie 4 (IndexedDB) + TipTap │
└─────────────────────────────────────────────────────────────┘
↑↓ /api/*
┌─────────────────────────────────────────────────────────────┐
│ バックエンド FastAPI ^0.136 + SQLAlchemy ^2.0 + │
│ Pydantic v2 + Alembic + Fernet │
└─────────────────────────────────────────────────────────────┘
↑↓ hookspecs
┌─────────────────────────────────────────────────────────────┐
│ PluginForge ^0.10.0 (外部PyPI; target_applicationに │
│ よるアイデンティティゲート) │
└─────────────────────────────────────────────────────────────┘
↑↓ entry_points
┌─────────────────────────────────────────────────────────────┐
│ プラグイン plugins/配下の10パッケージ │
│ (ai-{anthropic,openai,gemini}, assessment,│
│ session, tracking, tools, gamification, │
│ anki, notebooklm) │
└─────────────────────────────────────────────────────────────┘
新機能は常にプラグインに属します。ただし、コアに触れるもの(ユーザー / プロジェクト / 設定 / カリキュラム / トピック / レッスン / バックアップ / 同期 / システム / インポート)は除きます。
デュアルストレージ(v0.7.0)¶
フロントエンドには、バッキングストアを選択する単一の接合点があります: getStorage(): IStorageService。2つの実装が1つのコントラクトを満たします。
apiStorage(デフォルト): FastAPIバックエンドと通信するapi/client.tsの薄いラッパー。dexieStorage(ローカルファースト): 25のSQLAlchemyモデルをすべてミラーリングする完全なIndexedDBスタック。AI呼び出しはstorage/ai-providers.ts経由でブラウザから直接実行されます。
IStorageServiceは22個の名前空間を公開します(users、projects、settings、assessment、ストリーミング付きsession、tracking、tools、curricula、topics、lessons、plugins、system、backup、export、subjects、tags、projectTaxonomy、imports、gamification、anki、pronunciation、notebooklm)。両方のバッキングがすべてのメソッドを実装しています。
ファクトリーはlocalStorage["adaptive-learner.storage_mode"]、次にVITE_STORAGE_MODE(GH PagesビルドによってセットされるもM)、最後にデフォルトのapiの順で読み取ります。モードの切り替えはライブスワップではありません: Settingsページが選択を保存し、再読み込みが必要である旨のトーストを表示します。
3層シークレット(v1.20.0 / Phase 34)¶
すべてのAI呼び出しはservices/settings.resolve_api_key経由でチェーンを確認します。
ADAPTIVE_LEARNER_<PROVIDER>_API_KEY環境変数。~/.config/adaptive_learner/secrets.yamlのai.<provider>.api_key。- Fernet復号化済みDBカラム。
None— AI呼び出しがUIにエラーを表示します。
ソースの帰属はUserSettingsOut.key_source_*(列挙型: env / secrets_yaml / settings / none)に記録されます。ソースがenvまたはsecrets_yamlの場合、Settings UIはSave / Removeを無効にします。
同じチェーンがプロバイダーごとのdefault_modelオーバーライドにも適用されます。Phase 34の設計ではsecrets.yamlがUIオーバーライドより優先されます(パワーユーザー向けにファイル設定がUIより優先されます)。
プラグイン構造¶
plugins/adaptive-learner-plugin-<name>/
adaptive_learner_<name>/
plugin.py # <Name>Plugin(BasePlugin)、フックの実装
routes.py # FastAPIルーター(サービス関数に委譲)
<module>.py # ビジネスロジック
tests/
test_*.py # pytestテスト
pyproject.toml # エントリーポイント: [project.entry-points."adaptive_learner.plugins"]
- プラグインクラスは
BasePlugin(pluginforge)を継承します。 - ビジネスロジックは独自のモジュールに置き、routes.pyには置きません。
- routes.pyには委譲するFastAPIエンドポイントのみを含みます。
- フックスペックは
backend/app/hookspecs.pyに記述します。 - プラグインの依存関係はクラス属性として:
depends_on = ["session"]。 - すべてのプラグインは無料(MIT)。ライセンスインフラは存在しますが休止中(
LICENSING_ENABLED = False)。
フック(backend/app/hookspecs.pyの8スペック)¶
| フック | タイミング | ファーストリザルト? |
|---|---|---|
get_assessment_questions(lang) |
評価ページ読み込み | yes |
calculate_profile(answers) |
評価送信 | yes |
create_session_prompt(...) |
各チャットターン | yes |
ai_complete(messages, model, api_key, max_tokens) |
標準AI呼び出し | yes(プロバイダーはモデルプレフィックスでルーティング) |
ai_complete_async(...) |
並列サイクル境界評価(v1.5.0) | yes |
ai_complete_stream(...) |
ストリーミングセッション返答(v1.6.0) | yes |
recommend_method_switch(...) |
ダッシュボード + セッション | yes |
on_session_complete(session, rating) |
セッション終了 | broadcast |
get_progress_summary(project_id) |
ダッシュボードウィジェット | broadcast |
get_tool_recommendations(profile, lang) |
ダッシュボードツール | broadcast |
データフロー¶
UI (React) → IStorageService
→ (APIモード)FastAPIルーター → サービス → SQLAlchemy → SQLite
→ (Dexieモード)Dexieテーブル → IndexedDB
↓
AIオーケストレーター → resolve_api_key (env > yaml > DB)
→ pluginforge → プロバイダープラグインのai_complete*
→ Anthropic / OpenAI / Gemini SDK
一方向です。ルーターからDBに直接アクセスしません(サービスがSQLAlchemyの作業を担います)。バックエンドにフロントエンドのコードはありません。
エラー処理¶
フロントエンド ApiError (status + detail) → ユーザーへのトースト
APIクライアント HTTPエラー → ApiErrorに変換
ルーター 薄い層で何もキャッチしない。グローバル例外ハンドラーがマッピング。
サービス AdaptiveLearnerErrorのサブクラスをスロー
プラグイン PluginError(plugin_name, message)をスロー
外部 ExternalServiceError(service, message)でプロバイダーSDKを扱う
サービスはHTTPExceptionを絶対にスローしません; ルーターは何もキャッチしません。main.pyのグローバル例外ハンドラーがドメインエラーをHTTPステータスコードにマッピングします。完全なパターンは.claude/rules/code-hygiene.mdを参照してください。
永続化¶
- バックエンド: SQLAlchemy + SQLite。Alembicマイグレーションは
backend/migrations/versions/。 - 同期サーフェス: 28テーブル(v1.19.0ベースライン)。追記専用の履歴行(sessions、messages、ratings、progress commits、step evaluations、method switches、imported conversations、imported messages、anki cards、study questions)とミュータブルな設定+カリキュラム行。
- バックアップ形式: JSON; APIキーはエクスポート時に除去; リストアはマージ。
- テスト分離: 本番データディレクトリには
.adaptive-learner-productionマーカーがあり、テストがそれを検出した場合、pytest.exit(returncode=2)で実行が中断されます。
テーマ¶
5テーマ(Classic、Cool Modern、Nord、Notebook、Studio)× ライト/ダーク = 10バリアント。全体的にCSS変数を使用し、Tailwindは使用しません。カスタムプロパティはfrontend/src/styles/global.cssに記述します。新しいUI要素は変数セットを必ず使用してください。
モバイル / PWA¶
@media (max-width: 768px)が正規のモバイル切り替え点(ハンバーガードロワー、44×44タッチターゲット、スタックレイアウト)です。@media (max-width: 360px)は極狭画面のセーフネットです。デスクトップスタイル(≥769px)は変更なし。
サービスワーカー(Workbox via vite-plugin-pwa): GET /api/にNetworkFirstで4秒タイムアウト、24時間LRU、60エントリ上限。/api/へのミューテーションはNetworkOnlyです。