# Project export: LifeLearn

This document was generated by HackStack to give an AI agent context about a hackathon project. Sections are labeled with their provenance; content marked as truncated was cut to keep this document small.

## Project metadata

- Hackathon: TreeHacks 2026
- Tagline: continual learning in llms. solved.
- Devpost: https://devpost.com/software/lifelearn
- GitHub: https://github.com/Honyant/life-learn
- Demo: https://1hjt9zwi55af20-8000.proxy.runpod.net/
- Team: 1 GitHub contributor(s) — Anthony Wang (28 commits)

## Devpost submission (written by the team)

### Inspiration

LLMs can't learn from corrections, which is kind of absurd when you think about it. You tell it it's wrong, it apologizes, and then next conversation it makes the exact same mistake because nothing about the model actually changed. The SDFT paper (Shenfeld et al., ICLR 2025) showed that self-distillation lets you fine-tune without catastrophic forgetting, but it was all benchmark experiments on curated datasets. We wanted to see what happens when you put that in a real chat loop where a user just talks to the model and corrects it naturally.

### What it does

You talk to a local model, and if you correct it, the system picks up on that, generates training data around the correction, and fine-tunes in the background while you keep chatting. The header shows a progress bar during training and flips green when it's done. Multiple chat sessions are stored in SQLite but the corrections carry across all of them because it's the model weights themselves that changed, not some per-session prompt injection.

### How we built it

FastAPI backend running on a single GPU, vanilla HTML/CSS/JS frontend with a sidebar for sessions and a live training indicator. No React, no framework, nothing fancy. The training pipeline is where the work went. We adapted HuggingFace TRL's DistilTrainer to do SDFT: when a correction fires, GPT-4o-mini handles the structured parts (detecting what went wrong, generating prompt variations, writing expert demos), and then the local Qwen2.5-7B trains against a demo-conditioned copy of itself using reverse KL divergence. Teacher weights are an EMA of the student so they stay current, full fine-tuning in bfloat16, no LoRA. We serve chat during training by hooking into the trainer callback and running inference between optimizer steps, which is hacky but means the user never has to sit there waiting for training to finish.

### Challenges we ran into

The model kept getting worse at unrelated tasks after corrections, which is exactly what SDFT is supposed to prevent, so we had to figure out what was going wrong with our implementation versus the paper. The biggest issue was that the KL divergence was pointing the wrong direction. Forward KL computes the loss weighted by the teacher's probabilities across the entire vocabulary at every token position, so even positions that have nothing to do with your correction get pushed around, and over multiple corrections those small shifts compound into real degradation. Reverse KL weights by the student's own probabilities instead, which means if the student and teacher already agree on a token then basically nothing happens and the update stays focused on what actually matters. There were a couple other things too: the teacher model was frozen at init instead of being an EMA that tracks the student, and the teacher prompt said "short, direct response" which made it generate in a totally different style from the student, widening the gap between their distributions in ways that had nothing to do with the actual correction.

### Accomplishments we're proud of

The model genuinely accumulates corrections. You can correct it on three different topics and it retains all three while still being normal on everything else, which is the core promise of the paper and we got it working in a live interactive setting. The background training UX is also something we're happy with since gradient descent is literally running behind your conversation and the only indication is a progress bar filling up.

### What we learned

The direction of KL divergence matters way more than we expected. The paper writes reverse KL in its equations but the reference code actually ships forward KL, and that works fine when you train on hundreds of diverse examples because the noise at irrelevant token positions cancels out across the dataset. With 10 examples about one fact it doesn't cancel, everything drifts, and you get the forgetting you were trying to avoid. The other thing is that on-policy learning only prevents forgetting when the teacher distribution stays close to the student's, and there are a surprising number of ways to accidentally violate that (bad prompt template, frozen teacher weights, wrong KL direction) where each one independently brings the forgetting back.

### What's next

Adding a replay buffer so each training run mixes in some data from past corrections, since reverse KL handles most of the forgetting but an explicit anchor would help as you get into dozens of updates. After that we want to extend beyond factual corrections to style and reasoning, and let the model flag low-confidence answers proactively so it can ask for help before getting something wrong.

## README (from the GitHub repository)

# Life-Learn: SDFT Chat Correction Pipeline

A backend that demonstrates **Self-Distillation Fine-Tuning (SDFT)** triggered by user corrections in chat. When a user corrects the model, the system detects the correction, generates expert demonstrations, runs on-policy SDFT, and the model permanently learns from the correction.

Based on the paper ["Self-Distillation Enables Continual Learning"](https://arxiv.org/abs/2601.19897) by Shenfeld et al.

## How It Works

```
User corrects the model in chat
    -> Correction Detector (local LLM classifier)
    -> Prompt Augmenter (local LLM generates diverse question variations)
    -> Expert Demo Generator (GPT-4o-mini produces correct demonstrations)
    -> Data Formatter (HuggingFace Dataset with prompt + teacher_prompt)
    -> SDFT Trainer (on-policy distillation via DistilTrainer)
    -> Verification (fresh context, analogous question -- model should get it right)
```

During SDFT training (handled internally by DistilTrainer):
- **Student** generates on-policy rollouts from the prompt alone
- **Teacher** (same model, EMA weights) is conditioned on prompt + expert demo
- Loss: KL divergence between student and teacher distributions
- Teacher weights track student via exponential moving average

## Setup

### 1. Clone

```bash
git clone https://github.com/Honyant/life-learn.git
cd life-learn
```

### 2. Create conda environment

```bash
conda env create -f environment.yml
conda activate sdft
```

Or manually:

```bash
conda create -n sdft python=3.10
conda activate sdft
pip install -r requirements.txt
pip install pytest openai
```

### 3. Set OpenAI API key

```bash
export OPENAI_API_KEY="sk-..."
```

Or place it in `~/Documents/temp_dir/.env`:
```
export OPENAI_API_KEY="sk-..."
```

## Usage

### Multi-tenant Web Workspace (new)

This repo now includes a full-stack workspace with:

- secure authentication (session cookies + CSRF)
- multi-organization tenancy with role-based access
- invite-based member management per org
- multi-chat threads per organization
- per-organization model versioning and rollback
- correction-triggered continual learning jobs (SDFT)

Run it with:

```bash
uvicorn sdft_platform.main:app --host 0.0.0.0 --port 8080 --reload
```

Then open `http://localhost:8080`.

Environment variables (optional):

| Variable | Default | Description |
|---|---|---|
| `LL_DATABASE_URL` | `sqlite:///life_learn.db` | Database connection string |
| `LL_SECRET_KEY` | `dev-secret-change-me` | App secret (set a strong value in production) |
| `LL_INFERENCE_BACKEND` | `mock` | `mock`, `local`, or `openai` |
| `LL_BASE_MODEL_NAME` | `Qwen/Qwen2.5-7B-Instruct` | Base model ID/path for new orgs |
| `LL_OPENAI_MODEL` | `gpt-4o-mini` | OpenAI model for API-based inference/structured tasks |
| `LL_USE_LOCAL_STRUCTURED` | `false` | Use local model for correction detection/augmentation |
| `LL_MODEL_STORAGE_DIR` | `sdft_correction/output/org_models` | Per-org trained model storage root |

### Run unit tests (no GPU needed)

```bash
python -m pytest sdft_correction/test_pipeline.py -v -k 'not gpu'
```

### Run full integration test (GPU + API key)

```bash
python -m pytest sdft_correction/test_pipeline.py -v -m gpu -s
```

### Interactive demo

```bash
python -m sdft_correction.chat
```

Chat with the model. When you correct it, the pipeline triggers automatically:
1. Detects the correction
2. Generates 8 prompt variations via the local model
3. Gets 4 expert demos per prompt from GPT-4o-mini (~36 training pairs)
4. Runs SDFT training
5. Loads the trained model and verifies on an analogous question

## Project Structure

```
.
├── distil_config.py          # SDFT config (patched: added full_logit_distillation field)
├── distil_trainer.py         # DistilTrainer from the Self-Distillation paper
├── main.py                   # Original paper's training script (reference)
├── sdft_platform/            # New multi-tenant web workspace (FastAPI + UI)
├── environment.yml           # Conda environment file
├── requirements.txt          # Pip dependencies
└── sdft_correction/          # Chat correction pipeline
    ├── config.py             # PipelineConfig (model, paths, hyperparams)
    ├── inference.py          # Local model wrapper for generation
    ├── correction_detector.py # LLM-based correction classifier
    ├── augmenter.py          # Generates diverse prompt variations
    ├── expert_demos.py       # GPT-4o-mini expert demonstration generator
    ├── data_formatter.py     # Formats data for DistilTrainer
    ├── trainer.py            # SDFT training wrapper (full fine-tuning)
    ├── chat.py               # Interactive chat loop
    ├── conftest.py           # Pytest config
    └── test_pipeline.py      # 22 unit tests + 1 GPU integration test
```

## Configuration

Edit `sdft_correction/config.py` to change defaults:

| Parameter | Default | Description |
|---|---|---|
| `model_name` | `Qwen/Qwen2.5-0.5B-Instruct` | Base model (use 7B+ for real results) |
| `openai_model` | `gpt-4o-mini` | Expert demo generator |
| `learning_rate` | `5e-5` | SDFT learning rate |
| `num_train_epochs` | `2` | Training epochs |
| `gradient_accumulation_steps` | `8` | Effective batch size |
| `num_prompt_variations` | `8` | Augmented prompts per correction |
| `num_expert_demos_per_prompt` | `4` | Expert demos per prompt |

## Key Design Decisions

- **On-policy student rollouts**: `generate_from_teacher=False` -- the student generates its own completions during training, matching the paper's Algorithm 1
- **External expert demos**: GPT-4o-mini provides the demonstration context `c` for the teacher, analogous to how the paper uses GPT-4o for SciKnowEval
- **Full fine-tuning**: No LoRA, matching the paper's experimental setup. Use larger GPUs for 7B+ models.
- **Forward KL** (`alpha=0.0`): Matches the reference `main.py` implementation
- **EMA teacher** (`sync_ref_model=True`, `ref_model_mixup_alpha=0.01`): Teacher tracks student progress while smoothing updates

## Original Paper

This project builds on the Self-Distillation repo by [Shenfeld et al.](https://github.com/idanshen/Self-Distillation):

> **Self-Distillation Enables Continual Learning** (ICLR 2025)
> Idan Shenfeld, Mehul Damani, Jonas Hubotter, Pulkit Agrawal
> https://arxiv.org/abs/2601.19897


## Detected evidence (automated analysis)

Indexed codebase: 35 recognized source files, 289 KB.
- CSS (language) — detected in the code
- FastAPI (technology) — detected in the code
- HTML (language) — detected in the code
- Hugging Face (technology) — detected in the code
- JavaScript (language) — detected in the code
- OpenAI (technology) — detected in the code
- Python (language) — detected in the code
- PyTorch (technology) — detected in the code

## Codebase structure (from repository index)

### Files (39 of 39)

```
.gitignore
data/tooluse_data/eval_data.json
data/tooluse_data/train_data.json
distil_config.py
distil_trainer.py
environment.yml
main.py
README.md
requirements.txt
sdft_correction/__init__.py
sdft_correction/augmenter.py
sdft_correction/chat.py
sdft_correction/config.py
sdft_correction/conftest.py
sdft_correction/correction_detector.py
sdft_correction/data_formatter.py
sdft_correction/expert_demos.py
sdft_correction/inference.py
sdft_correction/test_pipeline.py
sdft_correction/trainer.py
sdft_platform/__init__.py
sdft_platform/db.py
sdft_platform/deps.py
sdft_platform/main.py
sdft_platform/models.py
sdft_platform/routers/__init__.py
sdft_platform/routers/auth.py
sdft_platform/routers/chats.py
sdft_platform/routers/models.py
sdft_platform/routers/orgs.py
sdft_platform/schemas.py
sdft_platform/security.py
sdft_platform/services/__init__.py
sdft_platform/services/model_runtime.py
sdft_platform/services/training_service.py
sdft_platform/settings.py
sdft_platform/static/app.css
sdft_platform/static/app.js
sdft_platform/templates/index.html
```

### Dependencies

- requirements.txt: accelerate@==1.11.0, argon2-cffi@==23.1.0, datasets@==4.3.0, deepspeed@==0.18.4, fastapi@==0.129.0, flashinfer-python@==0.5.3, jinja2@==3.1.6, matplotlib@==3.10.7, numpy@==2.2.6, openai@==2.6.1, pandas@==2.3.3, peft@==0.17.1, rich@==14.2.0, scipy@==1.15.3, sqlalchemy@==2.0.38, torch@==2.9.0, tqdm@==4.67.1, transformers@==4.57.1, trl@==0.24.0, uvicorn@==0.34.0, vllm@==0.12.0, wandb@==0.22.2

### Recent commits (newest first)

- Fix async form reset in org and invite actions
- Bump FastAPI to resolve Starlette conflict on RunPod
- Add multi-tenant continual-learning web workspace
- Constant LR with no warmup
- Fix continual learning: pass in-memory model between corrections, no disk save needed
- lr 1e-5, 8 epochs (~16 steps)
- 10 epochs for ~20 training steps
- Fix model collapse: lr 5e-5->5e-6, add repetition_penalty=1.3, cap max_new_tokens=150
- Disable all auto-saving; add /save command for explicit saves only
- Drop vLLM, use HF generate directly; 4 epochs for ~8 training steps
- Lazy save: defer model save to quit/unload so inference starts instantly after training
- Save model in background thread so vLLM inference starts immediately
- 3 epochs for ~6 training steps
- Set num_generations=1 to fix batch size divisibility error
- 1 epoch, no grad accumulation: ~3 steps total
- Auto-resume from trained checkpoint on startup if it exists
- Reuse trainer's vLLM for verification + chat instead of reloading from disk
- Fewer samples (10), lower grad_accum (2), 3 epochs: faster with more weight updates
- Remove 'including the thinking process' from teacher template — use short direct responses
- Generate short direct questions, not analytical ones; reduce max_completion_length to 80

## Key source files (fetched from GitHub, selected and truncated for size)

### requirements.txt

```
datasets==4.3.0
torch==2.9.0
transformers==4.57.1
accelerate==1.11.0
peft==0.17.1
rich==14.2.0
vllm==0.12.0
trl==0.24.0
openai==2.6.1
tqdm==4.67.1
numpy==2.2.6
scipy==1.15.3
matplotlib==3.10.7
flashinfer-python==0.5.3
wandb==0.22.2
pandas==2.3.3
deepspeed==0.18.4
fastapi==0.129.0
uvicorn==0.34.0
sqlalchemy==2.0.38
jinja2==3.1.6
argon2-cffi==23.1.0

```

### main.py

```python
from distil_trainer import DistilTrainer
from distil_config import DistilConfig
from transformers import AutoModelForCausalLM, AutoTokenizer
import torch
from datasets import Dataset, load_dataset, load_from_disk
from string import Template
import argparse
import torch.distributed as dist

def parse_args():
    parser = argparse.ArgumentParser(description="Distil Trainer")
    parser.add_argument("--learning_rate", type=float, default=2e-5, help="Learning rate")
    parser.add_argument("--num_train_epochs", type=int, default=1, help="Number of training epochs")
    parser.add_argument("--num_prompts_per_batch", type=int, default=32, help="Number of prompts per batch")
    parser.add_argument("--ref_model_mixup_alpha", type=float, default=0.01, help="Reference model mixup alpha")
    parser.add_argument("--output_dir", type=str, help="Output directory")
    parser.add_argument("--model_name", type=str, default="Qwen/Qwen2.5-7B-Instruct", help="Model name")
    parser.add_argument("--seed", type=int, default=42, help="Seed")
    return parser.parse_args()

def load_tooluse_dataset(seed=42) -> Dataset:
    """Load and prepare tooluse dataset with formatted prompts."""
    train_path = 'data/tooluse_data/train_data.json'
    test_path = 'data/tooluse_data/eval_data.json'
    train_dataset = Dataset.from_json(train_path)
    test_dataset = Dataset.from_json(test_path)

    def format_example(example):

        teacher_prompt = Template("""
$orig_content

This is an example for a response to the question:
$output_text

Now answer with a response of your own, including the thinking process.
""")

        return {
            "prompt": [{"role": "user", "content": example['prompt']}],
            "teacher_prompt": [{"role": "user", "content": teacher_prompt.substitute(orig_content=example['prompt'], output_text='\n'.join(example['golden_response']))}],
        }
    
    train_dataset = train_dataset.map(format_example, remove_columns=train_dataset.column_names)
    train_dataset = train_dataset.shuffle(seed=seed)
    return train_dataset, None


if __name__ == "__main__":
    args = parse_args()
    model = AutoModelForCausalLM.from_pretrained(
        args.model_name,
        torch_dtype=torch.bfloat16,
    )
    teacher_model = AutoModelForCausalLM.from_pretrained(
        args.model_name,
        torch_dtype=torch.bfloat16,
    )
    tokenizer = AutoTokenizer.from_pretrained(args.model_name)
    dataset = load_tooluse_dataset(args.seed)

    config = DistilConfig(
        seed=args.seed,
        use_vllm = True,
        vllm_mode="colocate",
        vllm_tensor_parallel_size=1, 
        vllm_gpu_memory_utilization=0.3,
        vllm_enable_sleep_mode=True, 
        learning_rate = args.learning_rate,
        warmup_ratio = 0.1,
        lr_scheduler_type = "cosine",
        logging_steps = 1,
        bf16 = True,
        fp16 = False,
        per_device_train_batch_size = 1,
        gradient_accumulation_steps = args.num_prompts_per_batch,
        max_prompt_length = 1024,
        max_completion_length = 1024,
        num_train_epochs = args.num_train_epochs,
        save_steps = 100,
        max_grad_norm = 1,
        report_to = "wandb",
        output_dir = args.output_dir,
        log_completions = False, # True for debugging
        sync_ref_model = True,
        ref_model_sync_steps = 1,
        ref_model_mixup_alpha = args.ref_model_mixup_alpha,
        vllm_importance_sampling_correction = True,
        num_loss_tokens_to_skip = 3,
    )
    trainer = DistilTrainer(
        model=model,
        ref_model=teacher_model,
        args=config,
        train_dataset=dataset,
        processing_class=tokenizer,
    )
    trainer.train()

```

### sdft_platform/main.py

```python
from __future__ import annotations

from pathlib import Path

from fastapi import FastAPI, Request
from fastapi.responses import HTMLResponse
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates

from sdft_platform.db import SessionLocal, init_db
from sdft_platform.routers.auth import router as auth_router
from sdft_platform.routers.chats import router as chats_router
from sdft_platform.routers.models import router as models_router
from sdft_platform.routers.orgs import router as orgs_router
from sdft_platform.services.model_runtime import OrgModelRuntime
from sdft_platform.services.training_service import TrainingCoordinator
from sdft_platform.settings import get_settings


settings = get_settings()
app = FastAPI(title=settings.app_name, version="0.1.0")

base_dir = Path(__file__).resolve().parent
templates = Jinja2Templates(directory=str(base_dir / "templates"))
app.mount("/static", StaticFiles(directory=str(base_dir / "static")), name="static")

app.include_router(auth_router)
app.include_router(orgs_router)
app.include_router(chats_router)
app.include_router(models_router)


@app.on_event("startup")
def on_startup() -> None:
    init_db()
    settings.model_storage_dir.mkdir(parents=True, exist_ok=True)
    app.state.model_runtime = OrgModelRuntime(settings)
    app.state.training_coordinator = TrainingCoordinator(
        settings=settings,
        session_factory=SessionLocal,
        model_runtime=app.state.model_runtime,
    )


@app.on_event("shutdown")
def on_shutdown() -> None:
    runtime = getattr(app.state, "model_runtime", None)
    if runtime is not None:
        runtime.shutdown()
    coordinator = getattr(app.state, "training_coordinator", None)
    if coordinator is not None:
        coordinator.shutdown()


@app.get("/", response_class=HTMLResponse)
def index(request: Request) -> HTMLResponse:
    return templates.TemplateResponse(
        request,
        "index.html",
        {
            "app_name": settings.app_name,
            "inference_backend": settings.inference_backend,
        },
    )


@app.get("/invite/{token}", response_class=HTMLResponse)
def invite_landing(request: Request, token: str) -> HTMLResponse:
    return templates.TemplateResponse(
        request,
        "index.html",
        {
            "app_name": settings.app_name,
            "inference_backend": settings.inference_backend,
            "invite_token": token,
        },
    )

```

### sdft_platform/static/app.js

```javascript
const state = {
  user: null,
  csrfToken: null,
  orgs: [],
  currentOrgId: null,
  threads: [],
  currentThreadId: null,
  pendingInviteToken: null,
  pollTimer: null,
};

const el = {
  toast: document.getElementById("toast"),
  authScreen: document.getElementById("auth-screen"),
  workspace: document.getElementById("workspace"),
  loginForm: document.getElementById("login-form"),
  registerForm: document.getElementById("register-form"),
  logoutBtn: document.getElementById("logout-btn"),
  userAvatar: document.getElementById("user-avatar"),
  userName: document.getElementById("user-name"),
  userEmail: document.getElementById("user-email"),
  orgList: document.getElementById("org-list"),
  newOrgForm: document.getElementById("new-org-form"),
  threadList: document.getElementById("thread-list"),
  newChatBtn: document.getElementById("new-chat-btn"),
  chatTitle: document.getElementById("chat-title"),
  chatSubtitle: document.getElementById("chat-subtitle"),
  messageList: document.getElementById("message-list"),
  composerForm: document.getElementById("composer-form"),
  composerInput: document.getElementById("composer-input"),
  correctionToggle: document.getElementById("correction-toggle"),
  modelVersion: document.getElementById("model-version"),
  modelStatus: document.getElementById("model-status"),
  modelPath: document.getElementById("model-path"),
  jobList: document.getElementById("job-list"),
  memberList: document.getElementById("member-list"),
  inviteForm: document.getElementById("invite-form"),
  inviteResult: document.getElementById("invite-result"),
};


function showToast(message, timeout = 2600) {
  el.toast.textContent = message;
  el.toast.classList.remove("hidden");
  window.clearTimeout(showToast._timer);
  showToast._timer = window.setTimeout(() => {
    el.toast.classList.add("hidden");
  }, timeout);
}


async function api(path, options = {}) {
  const headers = { ...(options.headers || {}) };
  const method = (options.method || "GET").toUpperCase();
  if (!headers["content-type"] && options.body !== undefined) {
    headers["content-type"] = "application/json";
  }
  if (!["GET", "HEAD", "OPTIONS"].includes(method) && state.csrfToken) {
    headers["x-csrf-token"] = state.csrfToken;
  }

  const response = await fetch(path, {
    ...options,
    method,
    credentials: "include",
    headers,
    body:
      options.body !== undefined && typeof options.body !== "string"
        ? JSON.stringify(options.body)
        : options.body,
  });

  if (response.status === 204) {
    return null;
  }

  let data = null;
  try {
    data = await response.json();
  } catch (_err) {
    data = null;
  }

  if (!response.ok) {
    throw new Error(data?.detail || `Request failed (${response.status})`);
  }
  return data;
}


function setAuthedUI(authed) {
  el.authScreen.classList.toggle("hidden", authed);
  el.workspace.classList.toggle("hidden", !authed);
}


function bindEvents() {
  el.loginForm.addEventListener("submit", onLogin);
  el.registerForm.addEventListener("submit", onRegister);
  el.logoutBtn.addEventListener("click", onLogout);
  el.newOrgForm.addEventListener("submit", onCreateOrg);
  el.newChatBtn.addEventListener("click", onCreateThread);
  el.composerForm.addEventListener("submit", onSendMessage);
  el.inviteForm.addEventListener("submit", onCreateInvite);
}


async function onLogin(event) {
  event.preventDefault();
  const formData = new FormData(event.currentTarget);
  try {
    const data = await api("/api/auth/login", {
      method: "POST",
      body: {
        email: formData.get("email"),
        password: formData.get("password"),
      },
    });
    await onAuthSuccess(data, "Welcome back");
  } catch (error) {
    showToast(error.message);
  }
}


async function onRegister(event) {
  event.preventDefault();
  const formData = new FormData(event.currentTarget);
  try {
    const data = await api("/api/auth/register", {
      method: "POST",
      body: {
        full_name: formData.get("full_name"),
        email: formData.get("email"),
        password: formData.get("password"),
      },
    });
    await onAuthSuccess(data, "Account created");
  } catch (error) {
    showToast(error.message);
  }
}


async function onAuthSuccess(data, toastText) {
  state.user = data.user;
  state.csrfToken = data.csrf_token;
  hydrateIdentity();
  setAuthedUI(true);
  await loadOrganizations();
  showToast(toastText);
  if (state.pendingInviteToken) {
    await acceptInvite(state.pendingInviteToken);
  }
}


async function onLogout() {
  try {
    await api("/api/auth/logout", { method: "POST" });
  } catch (_error) {
  }
  state.user = null;
  state.csrfToken = null;
  state.orgs = [];
  state.threads = [];
  state.currentOrgId = null;
  state.currentThreadId = null;
  clearInterval(state.pollTimer);
  setAuthedUI(false);
  renderOrgList();
  renderThreadList();
  renderMessages([]);
  showToast("Logged out");
}


function hydrateIdentity() {
  if (!state.user) {
    return;
  }
  el.userName.textContent = state.user.full_name;
  el.userEmail.textContent = state.user.email;
  el.userAvatar.textContent = (state.user.full_name || "U").slice(0, 1).toUpperCase();
}


async function restoreSession() {
  try {
    const data = await api("/api/auth/me");
    state.user = data.user;
    state.csrfToken = data.csrf_token;
    hydrateIdentity();
    setAuthedUI(true);
    await loadOrganizations();
  } catch (_error) {
    setAuthedUI(false);
  }
}


function orgNameById(orgId) {
  const org = state.orgs.find((entry) => entry.id === orgId);
  return org ? org.name : "";
}


async function loadOrganizations() {
  const orgs = await api("/api/orgs");
  state.orgs = orgs;
  renderOrgList();

  if (!state.currentOrgId || !state.orgs.some((org) => org.id === state.currentOrgId)) {
    state.currentOrgId = state.orgs[0]?.id ?? null;
  }

  if (state.currentOrgId) {
    await refreshOrgContext();
  } else {
    state.threads = [];
    state.currentThreadId = null;
    r
[truncated — 9505 more characters]
```

### environment.yml

```yaml
name: sdft
channels:
  - defaults
dependencies:
  - python=3.10
  - pip
  - pip:
    - datasets==4.3.0
    - torch==2.9.0
    - transformers==4.57.1
    - accelerate==1.11.0
    - peft==0.17.1
    - rich==14.2.0
    - vllm==0.12.0
    - trl==0.24.0
    - openai==2.6.1
    - tqdm==4.67.1
    - numpy==2.2.6
    - scipy==1.15.3
    - flashinfer-python==0.5.3
    - pytest
    - fastapi==0.129.0
    - uvicorn==0.34.0
    - sqlalchemy==2.0.38
    - jinja2==3.1.6
    - argon2-cffi==23.1.0

```

### sdft_platform/__init__.py

```python
"""Life-Learn multi-tenant chat platform package."""

```

### sdft_correction/conftest.py

```python
import pytest


def pytest_configure(config):
    config.addinivalue_line("markers", "gpu: marks tests as requiring GPU")

```

### sdft_platform/db.py

```python
from __future__ import annotations

from collections.abc import Generator

from sqlalchemy import create_engine
from sqlalchemy.orm import DeclarativeBase, Session, sessionmaker

from sdft_platform.settings import get_settings


class Base(DeclarativeBase):
    pass


settings = get_settings()
connect_args = {"check_same_thread": False} if settings.database_url.startswith("sqlite") else {}

engine = create_engine(settings.database_url, connect_args=connect_args, future=True)
SessionLocal = sessionmaker(bind=engine, autocommit=False, autoflush=False, expire_on_commit=False, class_=Session)


def get_db() -> Generator[Session, None, None]:
    db = SessionLocal()
    try:
        yield db
    finally:
        db.close()


def init_db() -> None:
    from sdft_platform import models  # noqa: F401

    Base.metadata.create_all(bind=engine)

```

### sdft_correction/config.py

```python
"""Shared configuration for the sdft-correction pipeline."""
from dataclasses import dataclass, field
from pathlib import Path


@dataclass
class PipelineConfig:
    # Models
    model_name: str = "Qwen/Qwen2.5-7B-Instruct"
    openai_model: str = "gpt-4o-mini"

    # When True, use the local model for correction detection and augmentation
    # instead of GPT-4o-mini. Works better with larger models (7B+).
    use_local_for_structured: bool = False

    # Paths — sdft_repo_path is the parent of this file's directory
    sdft_repo_path: Path = field(default_factory=lambda: Path(__file__).resolve().parent.parent)
    output_dir: Path = field(default_factory=lambda: Path(__file__).resolve().parent / "output")

    # SDFT training (no LoRA — full fine-tuning, matching the paper)
    learning_rate: float = 1e-5
    num_train_epochs: int = 8
    gradient_accumulation_steps: int = 1  # effective batch = 4
    max_prompt_length: int = 512
    max_completion_length: int = 80

    # Augmentation & expert demos
    num_prompt_variations: int = 4
    num_expert_demos_per_prompt: int = 2
    # Total training pairs: (num_prompt_variations + 1) * num_expert_demos_per_prompt
    # = 5 * 2 = 10

    # Inference
    max_new_tokens: int = 150
    temperature: float = 0.7

```

### sdft_correction/data_formatter.py

```python
"""Formats expert demonstrations into the SDFT training dataset format.

The dataset has two columns consumed by DistilTrainer:
  - ``prompt``         — what the *student* sees  (just the question)
  - ``teacher_prompt`` — what the *teacher* sees   (question + expert demo in-context)

During training the student generates an on-policy rollout from ``prompt``,
and the KL divergence to the teacher (conditioned on ``teacher_prompt``) is
minimised.
"""
from string import Template

from datasets import Dataset

# Matches the template in Self-Distillation/main.py:30-37 and paper Section 3.
TEACHER_PROMPT_TEMPLATE = Template(
    """$question

This is an example for a response to the question:
$demonstration

Now answer with a short, direct response of your own."""
)


def format_for_sdft(expert_demos: list) -> Dataset:
    """Convert a list of :class:`ExpertDemo` objects into a HuggingFace Dataset.

    Each row is one (student prompt, teacher prompt) pair.
    """
    prompts = []
    teacher_prompts = []

    for demo in expert_demos:
        teacher_content = TEACHER_PROMPT_TEMPLATE.substitute(
            question=demo.prompt,
            demonstration=demo.demonstration,
        )

        prompts.append([{"role": "user", "content": demo.prompt}])
        teacher_prompts.append([{"role": "user", "content": teacher_content}])

    return Dataset.from_dict(
        {
            "prompt": prompts,
            "teacher_prompt": teacher_prompts,
        }
    )

```

[23 more indexed source files omitted to keep this export small. The full file list is in the Codebase structure section above.]