mirror of
https://github.com/harvard-edge/cs249r_book.git
synced 2026-03-12 02:06:14 -05:00
Merge feature/tinytorch-core: fix Jupyter kernel mismatch (#1147)
This commit is contained in:
373
.github/workflows/all-contributors-add.yml
vendored
373
.github/workflows/all-contributors-add.yml
vendored
@@ -6,7 +6,12 @@
|
||||
# Uses Ollama LLM to parse natural language and extract:
|
||||
# - GitHub username (with or without @)
|
||||
# - Contribution type(s)
|
||||
# - Target project (optional, defaults based on issue context)
|
||||
#
|
||||
# Project detection is DETERMINISTIC (not LLM-guessed):
|
||||
# - PR file paths: tinytorch/ → tinytorch, book/ → book, kits/ → kits, labs/ → labs
|
||||
# - Explicit mention in comment: "in tinytorch", "for kits", etc.
|
||||
# - Issue labels/title context
|
||||
# - If none of the above → asks the user (never silently defaults)
|
||||
#
|
||||
# Flexible formats - all of these work:
|
||||
# @all-contributors @username helped verify the fix worked
|
||||
@@ -34,9 +39,8 @@ env:
|
||||
# Valid contribution types (comma-separated)
|
||||
CONTRIBUTION_TYPES: 'bug,code,doc,design,ideas,review,test,tool'
|
||||
|
||||
# Valid projects (comma-separated, first one is default)
|
||||
# Valid projects (comma-separated)
|
||||
PROJECTS: 'book,tinytorch,kits,labs'
|
||||
DEFAULT_PROJECT: 'book'
|
||||
|
||||
# Project aliases (format: alias1:project1,alias2:project2)
|
||||
PROJECT_ALIASES: 'tito:tinytorch'
|
||||
@@ -54,9 +58,15 @@ jobs:
|
||||
pull-requests: write
|
||||
|
||||
steps:
|
||||
- name: Extract trigger line and context
|
||||
# =====================================================================
|
||||
# STEP 1: Extract trigger line + detect project from PR files
|
||||
# =====================================================================
|
||||
- name: Extract trigger line and detect project
|
||||
id: extract
|
||||
uses: actions/github-script@v7
|
||||
env:
|
||||
PROJECTS: ${{ env.PROJECTS }}
|
||||
PROJECT_ALIASES: ${{ env.PROJECT_ALIASES }}
|
||||
with:
|
||||
script: |
|
||||
const body = context.payload.comment.body;
|
||||
@@ -73,19 +83,103 @@ jobs:
|
||||
|
||||
console.log('Trigger line:', triggerLine);
|
||||
|
||||
// Get issue context for project detection
|
||||
// --- Configuration ---
|
||||
const validProjects = process.env.PROJECTS.split(',');
|
||||
const projectAliases = {};
|
||||
if (process.env.PROJECT_ALIASES) {
|
||||
process.env.PROJECT_ALIASES.split(',').forEach(pair => {
|
||||
const [alias, proj] = pair.split(':');
|
||||
if (alias && proj) projectAliases[alias.trim()] = proj.trim();
|
||||
});
|
||||
}
|
||||
|
||||
// --- Helper: detect project name in text ---
|
||||
const detectProjectInText = (text) => {
|
||||
const lower = text.toLowerCase();
|
||||
for (const p of validProjects) {
|
||||
if (lower.includes(p)) return p;
|
||||
}
|
||||
for (const [alias, proj] of Object.entries(projectAliases)) {
|
||||
if (lower.includes(alias)) return proj;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
// --- Get issue/PR context ---
|
||||
const issue = context.payload.issue;
|
||||
const labels = issue.labels.map(l => l.name.toLowerCase());
|
||||
const title = issue.title.toLowerCase();
|
||||
const issueBody = issue.body ? issue.body.toLowerCase() : '';
|
||||
|
||||
// Build context string for LLM
|
||||
const issueContext = `Issue title: ${issue.title}\nLabels: ${labels.join(', ') || 'none'}`;
|
||||
|
||||
// =============================================================
|
||||
// PROJECT DETECTION (deterministic, priority order)
|
||||
// =============================================================
|
||||
let project = null;
|
||||
let projectSource = 'unknown';
|
||||
|
||||
// Priority 1: Explicit mention in the trigger comment
|
||||
const commentProject = detectProjectInText(triggerLine);
|
||||
if (commentProject) {
|
||||
project = commentProject;
|
||||
projectSource = 'comment';
|
||||
console.log(`Project from comment text: "${project}"`);
|
||||
}
|
||||
|
||||
// Priority 2: PR changed files (top-level dir → project)
|
||||
if (!project && issue.pull_request) {
|
||||
try {
|
||||
const { data: files } = await github.rest.pulls.listFiles({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
pull_number: issue.number,
|
||||
per_page: 100
|
||||
});
|
||||
|
||||
const projectCounts = {};
|
||||
for (const file of files) {
|
||||
const topDir = file.filename.split('/')[0];
|
||||
if (validProjects.includes(topDir)) {
|
||||
projectCounts[topDir] = (projectCounts[topDir] || 0) + 1;
|
||||
}
|
||||
}
|
||||
|
||||
const detected = Object.keys(projectCounts);
|
||||
console.log('PR file project counts:', JSON.stringify(projectCounts));
|
||||
|
||||
if (detected.length === 1) {
|
||||
project = detected[0];
|
||||
projectSource = 'pr_files';
|
||||
console.log(`Project from PR files: "${project}"`);
|
||||
} else if (detected.length > 1) {
|
||||
projectSource = 'ambiguous';
|
||||
console.log('PR spans multiple projects:', detected.join(', '));
|
||||
}
|
||||
} catch (e) {
|
||||
console.log('Could not fetch PR files:', e.message);
|
||||
}
|
||||
}
|
||||
|
||||
// Priority 3: Issue labels / title
|
||||
if (!project) {
|
||||
const contextProject = detectProjectInText(issueContext);
|
||||
if (contextProject) {
|
||||
project = contextProject;
|
||||
projectSource = 'issue_context';
|
||||
console.log(`Project from issue context: "${project}"`);
|
||||
}
|
||||
}
|
||||
|
||||
// If still null → projectSource stays 'unknown', handled downstream
|
||||
console.log(`Final project: ${project || 'NONE'} (source: ${projectSource})`);
|
||||
|
||||
core.setOutput('should_run', 'true');
|
||||
core.setOutput('trigger_line', triggerLine);
|
||||
core.setOutput('issue_context', issueContext);
|
||||
core.setOutput('project', project || '');
|
||||
core.setOutput('project_source', projectSource);
|
||||
|
||||
# =====================================================================
|
||||
# STEP 2: LLM parses username + contribution types (NOT project)
|
||||
# =====================================================================
|
||||
- name: Parse with LLM
|
||||
if: steps.extract.outputs.should_run == 'true'
|
||||
uses: ai-action/ollama-action@v2
|
||||
@@ -93,15 +187,10 @@ jobs:
|
||||
with:
|
||||
model: ${{ env.LLM_MODEL }}
|
||||
prompt: |
|
||||
Parse this contributor recognition comment and extract the required information.
|
||||
Parse this contributor recognition comment. Extract ONLY the username and contribution types.
|
||||
|
||||
COMMENT: ${{ steps.extract.outputs.trigger_line }}
|
||||
|
||||
ISSUE CONTEXT:
|
||||
${{ steps.extract.outputs.issue_context }}
|
||||
|
||||
TASK: Extract the GitHub username and contribution type(s) from the comment.
|
||||
|
||||
CONTRIBUTION TYPES (pick one or more):
|
||||
- bug: Found or reported a bug, identified issues
|
||||
- code: Wrote code, implemented features, fixed bugs
|
||||
@@ -112,156 +201,128 @@ jobs:
|
||||
- test: Tested features, verified fixes, QA testing
|
||||
- tool: Built tools, scripts, automation, CLI utilities
|
||||
|
||||
PROJECT OPTIONS: book, tinytorch, kits, labs
|
||||
- IMPORTANT: Look for project names in the comment! Phrases like "in tinytorch", "in Tinytorch", "for tinytorch", "tinytorch contributor" indicate the project
|
||||
- Also check issue context (title/labels) for project hints
|
||||
- Default to "book" ONLY if no other project is mentioned anywhere
|
||||
|
||||
OUTPUT FORMAT - Return ONLY a JSON object with exactly these 3 fields:
|
||||
Return ONLY a JSON object with exactly these 2 fields:
|
||||
{
|
||||
"username": "<github-username-without-@>",
|
||||
"types": ["<contribution-type>"],
|
||||
"project": "<project-name>"
|
||||
"types": ["<contribution-type>"]
|
||||
}
|
||||
|
||||
FIELD REQUIREMENTS:
|
||||
- username (string, required): GitHub username WITHOUT the @ symbol
|
||||
- types (array of strings, required): One or more of: bug, code, doc, design, ideas, review, test, tool
|
||||
- project (string, required): One of: book, tinytorch, kits, labs
|
||||
RULES:
|
||||
- username: The GitHub username WITHOUT the @ symbol. Ignore @all-contributors itself.
|
||||
- types: Array of one or more contribution types from the list above.
|
||||
- Do NOT include a "project" field. Project is detected separately.
|
||||
|
||||
EXAMPLES:
|
||||
Input: "@all-contributors @jane-doe fixed typos"
|
||||
Output: {"username": "jane-doe", "types": ["doc"], "project": "book"}
|
||||
Input: "@all-contributors @jane-doe fixed typos in the documentation"
|
||||
Output: {"username": "jane-doe", "types": ["doc"]}
|
||||
|
||||
Input: "@all-contributors please add @ngbolin for Doc in Tinytorch"
|
||||
Output: {"username": "ngbolin", "types": ["doc"], "project": "tinytorch"}
|
||||
|
||||
Input: "@all-contributors @user123 helped verify the fix worked in tinytorch"
|
||||
Output: {"username": "user123", "types": ["test"], "project": "tinytorch"}
|
||||
Output: {"username": "ngbolin", "types": ["doc"]}
|
||||
|
||||
Input: "@all-contributors @dev42 implemented the new feature and wrote tests"
|
||||
Output: {"username": "dev42", "types": ["code", "test"], "project": "book"}
|
||||
Output: {"username": "dev42", "types": ["code", "test"]}
|
||||
|
||||
Input: "@all-contributors please add @user123 for code"
|
||||
Output: {"username": "user123", "types": ["code"]}
|
||||
|
||||
Return ONLY the JSON object, no explanation or other text.
|
||||
|
||||
- name: Parse LLM response
|
||||
# =====================================================================
|
||||
# STEP 3: Parse LLM JSON + combine with deterministic project
|
||||
# =====================================================================
|
||||
- name: Parse LLM response and validate
|
||||
if: steps.extract.outputs.should_run == 'true'
|
||||
id: parse
|
||||
uses: actions/github-script@v7
|
||||
env:
|
||||
LLM_RESPONSE: ${{ steps.llm.outputs.response }}
|
||||
TRIGGER_LINE: ${{ steps.extract.outputs.trigger_line }}
|
||||
ISSUE_CONTEXT: ${{ steps.extract.outputs.issue_context }}
|
||||
PROJECT: ${{ steps.extract.outputs.project }}
|
||||
PROJECT_SOURCE: ${{ steps.extract.outputs.project_source }}
|
||||
CONTRIBUTION_TYPES: ${{ env.CONTRIBUTION_TYPES }}
|
||||
PROJECTS: ${{ env.PROJECTS }}
|
||||
DEFAULT_PROJECT: ${{ env.DEFAULT_PROJECT }}
|
||||
PROJECT_ALIASES: ${{ env.PROJECT_ALIASES }}
|
||||
with:
|
||||
script: |
|
||||
const response = process.env.LLM_RESPONSE || '';
|
||||
const triggerLine = process.env.TRIGGER_LINE || '';
|
||||
const issueContext = process.env.ISSUE_CONTEXT || '';
|
||||
console.log('LLM response:', response);
|
||||
|
||||
// Load configuration from environment
|
||||
const project = process.env.PROJECT || '';
|
||||
const projectSource = process.env.PROJECT_SOURCE || '';
|
||||
const validTypes = process.env.CONTRIBUTION_TYPES.split(',');
|
||||
const validProjects = process.env.PROJECTS.split(',');
|
||||
const defaultProject = process.env.DEFAULT_PROJECT;
|
||||
|
||||
// Parse project aliases (format: alias1:project1,alias2:project2)
|
||||
const projectAliases = {};
|
||||
if (process.env.PROJECT_ALIASES) {
|
||||
process.env.PROJECT_ALIASES.split(',').forEach(pair => {
|
||||
const [alias, project] = pair.split(':');
|
||||
if (alias && project) projectAliases[alias] = project;
|
||||
});
|
||||
}
|
||||
|
||||
console.log('LLM response:', response);
|
||||
console.log('Deterministic project:', project || 'NONE', `(source: ${projectSource})`);
|
||||
|
||||
let username = null;
|
||||
let types = [];
|
||||
let project = defaultProject;
|
||||
|
||||
// --- Parse LLM JSON response ---
|
||||
try {
|
||||
// Find JSON in response (LLM might add extra text)
|
||||
const jsonMatch = response.match(/\{[\s\S]*?\}/);
|
||||
if (jsonMatch) {
|
||||
const parsed = JSON.parse(jsonMatch[0]);
|
||||
|
||||
// Extract username
|
||||
if (parsed.username && typeof parsed.username === 'string') {
|
||||
username = parsed.username.replace(/^@/, ''); // Remove @ if present
|
||||
username = parsed.username.replace(/^@/, '');
|
||||
}
|
||||
|
||||
// Extract types
|
||||
if (parsed.types && Array.isArray(parsed.types)) {
|
||||
types = parsed.types.filter(t => validTypes.includes(t.toLowerCase())).map(t => t.toLowerCase());
|
||||
}
|
||||
|
||||
// Extract project
|
||||
if (parsed.project && validProjects.includes(parsed.project.toLowerCase())) {
|
||||
project = parsed.project.toLowerCase();
|
||||
types = parsed.types
|
||||
.map(t => t.toLowerCase().trim())
|
||||
.filter(t => validTypes.includes(t));
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.log('Failed to parse JSON:', e.message);
|
||||
console.log('Failed to parse LLM JSON:', e.message);
|
||||
}
|
||||
|
||||
// Fallback: try to extract username from trigger line if LLM failed
|
||||
// --- Fallback: extract username from @mentions ---
|
||||
if (!username) {
|
||||
const usernameMatch = triggerLine.match(/@(\w[-\w]*)/g);
|
||||
if (usernameMatch && usernameMatch.length > 1) {
|
||||
// Skip @all-contributors, take the next @mention
|
||||
username = usernameMatch[1].replace(/^@/, '');
|
||||
const mentions = triggerLine.match(/@([\w][\w-]*)/g);
|
||||
if (mentions && mentions.length > 1) {
|
||||
username = mentions[1].replace(/^@/, '');
|
||||
console.log('Username from @mention fallback:', username);
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: detect project from trigger line or issue context if not in LLM response
|
||||
// Priority: trigger line first (explicit user request), then issue context
|
||||
if (project === defaultProject) {
|
||||
const triggerLower = triggerLine.toLowerCase();
|
||||
const contextLower = issueContext.toLowerCase();
|
||||
|
||||
// Helper function to detect project in text
|
||||
const detectProject = (text) => {
|
||||
// Check for direct project names (except default)
|
||||
for (const p of validProjects) {
|
||||
if (p !== defaultProject && text.includes(p)) return p;
|
||||
}
|
||||
// Check for aliases
|
||||
for (const [alias, targetProject] of Object.entries(projectAliases)) {
|
||||
if (text.includes(alias)) return targetProject;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
// Check trigger line first, then issue context
|
||||
const detected = detectProject(triggerLower) || detectProject(contextLower);
|
||||
if (detected) project = detected;
|
||||
}
|
||||
|
||||
console.log('Username:', username);
|
||||
console.log('Types:', types);
|
||||
console.log('Project:', project);
|
||||
|
||||
// --- Validate username ---
|
||||
if (!username) {
|
||||
console.log('Could not determine username');
|
||||
core.setOutput('success', 'false');
|
||||
core.setOutput('error', 'no_username');
|
||||
return;
|
||||
}
|
||||
|
||||
// --- Validate types ---
|
||||
if (types.length === 0) {
|
||||
console.log('Could not determine contribution type');
|
||||
core.setOutput('success', 'false');
|
||||
core.setOutput('error', 'no_types');
|
||||
core.setOutput('username', username);
|
||||
return;
|
||||
}
|
||||
|
||||
// --- Validate project (deterministic — already resolved in Step 1) ---
|
||||
if (!project || !validProjects.includes(project)) {
|
||||
console.log('No valid project detected — will ask user');
|
||||
core.setOutput('success', 'false');
|
||||
core.setOutput('error', 'no_project');
|
||||
core.setOutput('username', username);
|
||||
core.setOutput('types', JSON.stringify(types));
|
||||
core.setOutput('project_source', projectSource);
|
||||
return;
|
||||
}
|
||||
|
||||
// --- All good ---
|
||||
console.log('✅ Final result:', { username, types, project, projectSource });
|
||||
|
||||
core.setOutput('success', 'true');
|
||||
core.setOutput('username', username);
|
||||
core.setOutput('types', JSON.stringify(types));
|
||||
core.setOutput('project', project);
|
||||
core.setOutput('project_source', projectSource);
|
||||
|
||||
# =====================================================================
|
||||
# STEP 4: Checkout, update config, generate READMEs, commit
|
||||
# =====================================================================
|
||||
- name: Checkout repository
|
||||
if: steps.extract.outputs.should_run == 'true' && steps.parse.outputs.success == 'true'
|
||||
uses: actions/checkout@v4
|
||||
@@ -401,6 +462,9 @@ jobs:
|
||||
echo "Changes committed and pushed!"
|
||||
fi
|
||||
|
||||
# =====================================================================
|
||||
# STEP 5: Post success comment
|
||||
# =====================================================================
|
||||
- name: React to comment
|
||||
if: steps.extract.outputs.should_run == 'true' && steps.parse.outputs.success == 'true'
|
||||
uses: actions/github-script@v7
|
||||
@@ -417,13 +481,23 @@ jobs:
|
||||
// Post a reply
|
||||
const username = '${{ steps.parse.outputs.username }}';
|
||||
const project = '${{ steps.parse.outputs.project }}';
|
||||
const projectSource = '${{ steps.parse.outputs.project_source }}';
|
||||
const types = JSON.parse('${{ steps.parse.outputs.types }}');
|
||||
const triggerLine = `${{ steps.extract.outputs.trigger_line }}`;
|
||||
|
||||
// Map source to human-readable explanation
|
||||
const sourceLabels = {
|
||||
comment: 'explicitly mentioned in comment',
|
||||
pr_files: 'detected from PR changed files',
|
||||
issue_context: 'detected from issue labels/title'
|
||||
};
|
||||
const sourceNote = sourceLabels[projectSource] || projectSource;
|
||||
|
||||
const body = [
|
||||
"I've added @" + username + " as a contributor to **" + project + "**! :tada:",
|
||||
"",
|
||||
"**Recognized for:** " + types.join(', '),
|
||||
"**Project:** " + project + " (" + sourceNote + ")",
|
||||
"**Based on:** " + triggerLine,
|
||||
"",
|
||||
"The contributor list has been updated in:",
|
||||
@@ -441,53 +515,86 @@ jobs:
|
||||
body: body
|
||||
});
|
||||
|
||||
# =====================================================================
|
||||
# STEP 6: Handle failures — ask user when project is unknown
|
||||
# =====================================================================
|
||||
- name: Handle parsing failure
|
||||
if: steps.extract.outputs.should_run == 'true' && steps.parse.outputs.success == 'false'
|
||||
uses: actions/github-script@v7
|
||||
env:
|
||||
PROJECTS: ${{ env.PROJECTS }}
|
||||
DEFAULT_PROJECT: ${{ env.DEFAULT_PROJECT }}
|
||||
with:
|
||||
script: |
|
||||
const error = '${{ steps.parse.outputs.error }}';
|
||||
const triggerLine = `${{ steps.extract.outputs.trigger_line }}`;
|
||||
const projects = process.env.PROJECTS.split(',');
|
||||
const defaultProject = process.env.DEFAULT_PROJECT;
|
||||
const projectSource = '${{ steps.parse.outputs.project_source }}' || '';
|
||||
const username = '${{ steps.parse.outputs.username }}' || '';
|
||||
const typesRaw = '${{ steps.parse.outputs.types }}' || '[]';
|
||||
const types = (() => { try { return JSON.parse(typesRaw); } catch { return []; } })();
|
||||
|
||||
let errorMsg = "I couldn't parse that comment.";
|
||||
if (error === 'no_username') {
|
||||
errorMsg = "I couldn't find a GitHub username in that comment.";
|
||||
} else if (error === 'no_types') {
|
||||
errorMsg = "I couldn't determine the contribution type.";
|
||||
let body;
|
||||
|
||||
if (error === 'no_project') {
|
||||
// === PROJECT UNKNOWN — ask the user ===
|
||||
const userPart = username ? ` @${username}` : '';
|
||||
const typesPart = types.length > 0 ? ` for ${types.join(', ')}` : ' for code';
|
||||
|
||||
if (projectSource === 'ambiguous') {
|
||||
// PR touches multiple projects
|
||||
body = [
|
||||
"This PR touches files in **multiple projects**, so I need you to tell me which one. :thinking:",
|
||||
"",
|
||||
`I detected${userPart}${typesPart}, but which project should I add them to?`,
|
||||
"",
|
||||
"Please reply with the project specified:",
|
||||
...projects.map(p => `- \`@all-contributors${userPart}${typesPart} in ${p}\``),
|
||||
].join('\n');
|
||||
} else {
|
||||
// No project signal at all (issue with no labels, no PR files)
|
||||
body = [
|
||||
`I couldn't determine which project to add the contributor to. :thinking:`,
|
||||
"",
|
||||
"**Your comment:** " + triggerLine,
|
||||
"",
|
||||
"This repo has multiple projects, so please specify which one:",
|
||||
...projects.map(p => `- \`@all-contributors${userPart}${typesPart} in ${p}\``),
|
||||
"",
|
||||
"**How project detection works:**",
|
||||
"- On **PRs**: auto-detected from changed file paths (`tinytorch/` → tinytorch, `book/` → book, etc.)",
|
||||
"- On **issues**: detected from labels or title, otherwise you need to specify explicitly",
|
||||
].join('\n');
|
||||
}
|
||||
} else {
|
||||
// === Other errors (no_username, no_types) ===
|
||||
let errorMsg = "I couldn't parse that comment.";
|
||||
if (error === 'no_username') {
|
||||
errorMsg = "I couldn't find a GitHub username in that comment.";
|
||||
} else if (error === 'no_types') {
|
||||
errorMsg = "I couldn't determine the contribution type.";
|
||||
}
|
||||
|
||||
body = [
|
||||
errorMsg + " :thinking:",
|
||||
"",
|
||||
"**Your comment:** " + triggerLine,
|
||||
"",
|
||||
"**Example formats that work:**",
|
||||
"```",
|
||||
"@all-contributors @jane-doe fixed typos in the documentation",
|
||||
"@all-contributors please add @john_smith for reviewing the PR",
|
||||
"@all-contributors @user123 helped verify the fix worked",
|
||||
"@all-contributors @dev42 implemented the new caching feature in tinytorch",
|
||||
"```",
|
||||
"",
|
||||
"**Contribution types I understand:**",
|
||||
"bug, code, doc, design, ideas, review, test, tool",
|
||||
"",
|
||||
`**Projects:** ${projects.join(', ')}`,
|
||||
"On PRs, project is auto-detected from file paths. Otherwise, specify explicitly (e.g., 'in tinytorch')."
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
const body = [
|
||||
errorMsg + " :thinking:",
|
||||
"",
|
||||
"**Your comment:** " + triggerLine,
|
||||
"",
|
||||
"**Example formats that work:**",
|
||||
"```",
|
||||
"@all-contributors @jane-doe fixed typos in the documentation",
|
||||
"@all-contributors please add @john_smith for reviewing the PR",
|
||||
"@all-contributors @user123 helped verify the fix worked",
|
||||
"@all-contributors @dev42 implemented the new caching feature",
|
||||
"```",
|
||||
"",
|
||||
"**Contribution types I understand:**",
|
||||
"- bug (found bugs, reported issues)",
|
||||
"- code (wrote code, fixed bugs)",
|
||||
"- doc (documentation, typos)",
|
||||
"- design (UI/UX, architecture)",
|
||||
"- ideas (suggestions, proposals)",
|
||||
"- review (code review, feedback)",
|
||||
"- test (testing, verification)",
|
||||
"- tool (built tools, automation)",
|
||||
"",
|
||||
`**Projects:** ${projects.join(', ')} (default: ${defaultProject})`,
|
||||
"Auto-detected from issue context, or mention it explicitly (e.g., 'in tinytorch')."
|
||||
].join('\n');
|
||||
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
|
||||
6
.github/workflows/tinytorch-validate-dev.yml
vendored
6
.github/workflows/tinytorch-validate-dev.yml
vendored
@@ -436,7 +436,11 @@ jobs:
|
||||
- name: Run Fresh Install Test (Docker)
|
||||
run: |
|
||||
chmod +x tinytorch/scripts/test-fresh-install.sh
|
||||
./tinytorch/scripts/test-fresh-install.sh --branch ${{ github.ref_name }}
|
||||
# For PRs, github.ref_name is the merge ref (e.g. "1159/merge") which doesn't
|
||||
# exist as a real branch on raw.githubusercontent.com. Use head_ref (the actual
|
||||
# source branch) for PRs, falling back to ref_name for push events.
|
||||
BRANCH="${{ github.head_ref || github.ref_name }}"
|
||||
./tinytorch/scripts/test-fresh-install.sh --branch "$BRANCH"
|
||||
|
||||
# ===========================================================================
|
||||
# STAGE 7: USER JOURNEY - Destructive full journey (after all stages)
|
||||
|
||||
@@ -961,15 +961,6 @@
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "AndreaMattiaGaravagno",
|
||||
"name": "AndreaMattiaGaravagno",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/22458187?v=4",
|
||||
"profile": "https://github.com/AndreaMattiaGaravagno",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -221,7 +221,7 @@
|
||||
|
||||
% Abstract - REVISED: Curriculum design focus
|
||||
\begin{abstract}
|
||||
Machine learning education faces a fundamental gap: students learn algorithms without understanding the systems that execute them. They study gradient descent without measuring memory, attention mechanisms without analyzing $O(N^2)$ scaling, optimizer theory without knowing why Adam requires $3\times$ the optimizer-related memory of SGD (gradients plus two state buffers). This \emph{algorithm-systems divide} produces practitioners who can train models but cannot debug memory failures, optimize inference latency, or reason about deployment trade-offs—the very skills industry demands as ``ML systems engineering.'' We present TinyTorch, a 20-module curriculum that closes this gap through \emph{implementation-based systems pedagogy}: students construct PyTorch's core components (tensors, autograd, optimizers, CNNs, transformers) in pure Python, building a complete framework where every operation they invoke is code they wrote. The design employs three patterns: \emph{progressive disclosure} of complexity, \emph{systems-first integration} of profiling from the first module, and \emph{build-to-validate milestones} recreating 67 years of ML breakthroughs—from Perceptron (1958) through Transformers (2017) to MLPerf-style benchmarking. Requiring only 4GB RAM and no GPU, TinyTorch demonstrates that deep ML systems understanding is achievable without specialized hardware. The curriculum is available open-source at \texttt{mlsysbook.ai/tinytorch}.
|
||||
Machine learning education faces a fundamental gap: students learn algorithms without understanding the systems that execute them. They study gradient descent without measuring memory, attention mechanisms without analyzing $O(N^2)$ scaling, optimizer theory without knowing why Adam requires $3\times$ the optimizer-related memory of SGD (gradients plus two state buffers). This \emph{algorithm-systems divide} produces practitioners who can train models but cannot debug memory failures, optimize inference latency, or reason about deployment trade-offs---the very skills industry demands as ``ML systems engineering.'' We present TinyTorch, a 20-module curriculum that closes this gap through \emph{implementation-based systems pedagogy}: students construct PyTorch's core components (tensors, autograd, optimizers, CNNs, transformers) in pure Python, building a complete framework where every operation they invoke is code they wrote. The design employs three patterns: \emph{progressive disclosure} of complexity, \emph{systems-first integration} of profiling from the first module, and \emph{build-to-validate milestones} recreating 67 years of ML breakthroughs---from Perceptron (1958) through Transformers (2017) to MLPerf-style benchmarking. Requiring only 4GB RAM and no GPU, TinyTorch demonstrates that deep ML systems understanding is achievable without specialized hardware. The curriculum is available open-source at \texttt{mlsysbook.ai/tinytorch}.
|
||||
\end{abstract}
|
||||
|
||||
% Main content
|
||||
@@ -844,7 +844,7 @@ The Optimization Tier completes the systems-first integration arc: students who
|
||||
Translating curriculum design into effective classroom practice requires addressing integration models, infrastructure accessibility, and student support structures. This section presents deployment patterns designed for diverse institutional contexts.
|
||||
|
||||
\textbf{Textbook Integration.}
|
||||
TinyTorch serves as the hands-on implementation companion to the \emph{Machine Learning Systems} textbook~\citep{mlsysbook2025} (\texttt{mlsysbook.ai}), creating synergy between theoretical foundations and systems engineering practice. While the textbook covers the full ML lifecycle—data engineering, training architectures, deployment monitoring, robust operations, and sustainable AI—TinyTorch provides the complementary experience of building core infrastructure from first principles. This integration enables a complete educational pathway: students study production ML systems architecture in the textbook (Chapter 4: distributed training patterns, Chapter 7: quantization strategies), then implement those same abstractions in TinyTorch (Module 06: autograd for backpropagation, Module 15: INT8 quantization). The two resources address different aspects of the same educational gap: understanding both \emph{how production systems work} (textbook's systems architecture perspective) and \emph{how to build them yourself} (TinyTorch's implementation depth).
|
||||
TinyTorch serves as the hands-on implementation companion to the \emph{Machine Learning Systems} textbook~\citep{mlsysbook2025} (\texttt{mlsysbook.ai}), creating synergy between theoretical foundations and systems engineering practice. While the textbook covers the full ML lifecycle---data engineering, training architectures, deployment monitoring, robust operations, and sustainable AI---TinyTorch provides the complementary experience of building core infrastructure from first principles. This integration enables a complete educational pathway: students study production ML systems architecture in the textbook (Chapter 4: distributed training patterns, Chapter 7: quantization strategies), then implement those same abstractions in TinyTorch (Module 06: autograd for backpropagation, Module 15: INT8 quantization). The two resources address different aspects of the same educational gap: understanding both \emph{how production systems work} (textbook's systems architecture perspective) and \emph{how to build them yourself} (TinyTorch's implementation depth).
|
||||
|
||||
\subsection{Integration Models}
|
||||
\label{subsec:integration}
|
||||
@@ -996,7 +996,7 @@ TinyTorch is released as open source to enable community adoption and evolution.
|
||||
|
||||
Effective deployment requires structured TA support beyond instructor guidance.
|
||||
|
||||
\textbf{TA Preparation}: TAs should develop deep familiarity with critical modules where students commonly struggle—Modules 06 (Autograd), 09 (CNNs), and 13 (Transformers)—by completing these modules themselves and intentionally introducing bugs to understand common error patterns. The \texttt{INSTRUCTOR.md} file documents frequent student errors (gradient shape mismatches, disconnected computational graphs, broadcasting failures) and debugging strategies.
|
||||
\textbf{TA Preparation}: TAs should develop deep familiarity with critical modules where students commonly struggle---Modules 06 (Autograd), 09 (CNNs), and 13 (Transformers)---by completing these modules themselves and intentionally introducing bugs to understand common error patterns. The \texttt{INSTRUCTOR.md} file documents frequent student errors (gradient shape mismatches, disconnected computational graphs, broadcasting failures) and debugging strategies.
|
||||
|
||||
\textbf{Office Hour Demand Patterns}: Student help requests are expected to cluster around conceptually challenging modules, with autograd (Module 06) likely generating higher office hour demand than foundation modules. Instructors should anticipate demand spikes by scheduling additional TA capacity during critical modules, providing pre-recorded debugging walkthroughs, and establishing async support channels (discussion forums with guaranteed response times).
|
||||
|
||||
|
||||
@@ -161,7 +161,7 @@
|
||||
title = {Computer Science Education Research},
|
||||
author = {Fincher, Sally and Petre, Marian},
|
||||
year = {2005},
|
||||
publisher = {Taylor & Francis},
|
||||
publisher = {Taylor \& Francis},
|
||||
address = {London},
|
||||
doi = {10.1201/9781482287325},
|
||||
isbn = {9781482287325},
|
||||
@@ -279,7 +279,7 @@
|
||||
}
|
||||
|
||||
@misc{keller2025ai,
|
||||
title = {{AI} & Machine-Learning Talent Gap 2025},
|
||||
title = {{AI} \& Machine-Learning Talent Gap 2025},
|
||||
author = {{Keller Executive Search}},
|
||||
year = {2025},
|
||||
url = {https://www.kellerexecutivesearch.com/intelligence/ai-machine-learning-talent-gap-2025/},
|
||||
|
||||
@@ -43,6 +43,8 @@ dev = [
|
||||
"nbformat>=5.10.0",
|
||||
"jupyter>=1.1.0",
|
||||
"jupyterlab>=4.2.0",
|
||||
"ipykernel>=6.29.0",
|
||||
"nbdev>=2.3.0",
|
||||
]
|
||||
visualization = [
|
||||
"matplotlib>=3.9.0", # For plotting in Modules 17, 19, 20
|
||||
|
||||
@@ -71,7 +71,12 @@ echo ""
|
||||
echo "▶ Step 1: Running install script (branch: $BRANCH)..."
|
||||
export TINYTORCH_BRANCH="$BRANCH"
|
||||
export TINYTORCH_NON_INTERACTIVE=1
|
||||
curl -sSL "https://raw.githubusercontent.com/harvard-edge/cs249r_book/${BRANCH}/tinytorch/site/extra/install.sh" -o /tmp/install.sh
|
||||
curl -fsSL "https://raw.githubusercontent.com/harvard-edge/cs249r_book/${BRANCH}/tinytorch/site/extra/install.sh" -o /tmp/install.sh || {
|
||||
echo "✗ Failed to download install script for branch: $BRANCH"
|
||||
echo " URL: https://raw.githubusercontent.com/harvard-edge/cs249r_book/${BRANCH}/tinytorch/site/extra/install.sh"
|
||||
echo " Hint: Does the branch '${BRANCH}' exist and contain tinytorch/site/extra/install.sh?"
|
||||
exit 1
|
||||
}
|
||||
bash /tmp/install.sh
|
||||
|
||||
cd tinytorch
|
||||
|
||||
@@ -156,8 +156,9 @@ def convert_py_to_notebook(module_path: Path, venv_path: Path, console) -> bool:
|
||||
console.print("[dim]🔄 Overwriting existing notebook (Python file is source of truth)[/dim]" if notebook_file.exists() else "[dim]✨ Creating new notebook from Python file[/dim]")
|
||||
|
||||
try:
|
||||
from ..core.virtual_env_manager import get_venv_bin_dir
|
||||
jupytext_path = "jupytext"
|
||||
venv_jupytext = venv_path / "bin" / "jupytext"
|
||||
venv_jupytext = get_venv_bin_dir(venv_path) / "jupytext"
|
||||
|
||||
if venv_jupytext.exists():
|
||||
test_result = subprocess.run([str(venv_jupytext), "--version"], capture_output=True, text=True)
|
||||
|
||||
@@ -1111,20 +1111,39 @@ class ModuleWorkflowCommand(BaseCommand):
|
||||
|
||||
# Run nbdev_export using Python API directly (more reliable than subprocess)
|
||||
from nbdev.export import nb_export
|
||||
|
||||
|
||||
self.console.print(f"[dim]📦 Exporting {notebook_path.name} → tinytorch/core/...[/dim]")
|
||||
|
||||
|
||||
lib_path = Path.cwd() / "tinytorch"
|
||||
nb_export(notebook_path, lib_path=lib_path)
|
||||
|
||||
|
||||
# Verify the export actually produced a file
|
||||
if export_target != "unknown":
|
||||
target_file = lib_path / (export_target.replace(".", "/") + ".py")
|
||||
if not target_file.exists():
|
||||
self.console.print(f"[red]❌ Export verification failed: {target_file} was not created[/red]")
|
||||
self.console.print(f"[dim] Expected from #| default_exp: {export_target}[/dim]")
|
||||
self.console.print("[yellow] Check that your notebook has #| export cells with code[/yellow]")
|
||||
return 1
|
||||
|
||||
# Verify the file has actual content (not empty)
|
||||
content = target_file.read_text(encoding="utf-8")
|
||||
code_lines = [l for l in content.split('\n')
|
||||
if l.strip() and not l.strip().startswith('#')]
|
||||
if len(code_lines) < 2:
|
||||
self.console.print(f"[red]❌ Export verification failed: {target_file} is empty[/red]")
|
||||
self.console.print("[yellow] Your notebook's #| export cells may not contain code[/yellow]")
|
||||
return 1
|
||||
|
||||
self.console.print(f"[dim]✅ Your code is now part of the tinytorch package![/dim]")
|
||||
return 0
|
||||
|
||||
|
||||
except ImportError:
|
||||
self.console.print("[red]❌ nbdev not found. Is your environment set up?[/red]")
|
||||
self.console.print("[red]❌ nbdev not found — cannot export module[/red]")
|
||||
self.console.print("[yellow] Fix: pip install nbdev[/yellow]")
|
||||
return 1
|
||||
except Exception as e:
|
||||
self.console.print(f"[red]Error exporting module: {e}[/red]")
|
||||
self.console.print(f"[red]❌ Export failed: {e}[/red]")
|
||||
return 1
|
||||
|
||||
def get_progress_data(self) -> dict:
|
||||
|
||||
@@ -134,6 +134,8 @@ class SetupCommand(BaseCommand):
|
||||
("jupyter", "jupyter>=1.0.0"),
|
||||
("jupyterlab", "jupyterlab>=3.0.0"),
|
||||
("jupytext", "jupytext>=1.13.0"),
|
||||
("ipykernel", "ipykernel>=6.29.0"),
|
||||
("nbdev", "nbdev>=2.3.0"),
|
||||
("rich", "rich>=12.0.0"),
|
||||
("pyyaml", "pyyaml>=6.0"),
|
||||
("psutil", "psutil>=5.8.0"),
|
||||
@@ -208,7 +210,6 @@ class SetupCommand(BaseCommand):
|
||||
|
||||
if result.returncode == 0:
|
||||
progress.update(task, description="[green]✅ Tiny🔥Torch installed[/green]")
|
||||
return True
|
||||
else:
|
||||
progress.update(task, description="[red]❌ Tiny🔥Torch install failed[/red]")
|
||||
self.console.print(f"[red]Failed to install Tiny🔥Torch: {result.stderr}[/red]")
|
||||
@@ -219,6 +220,36 @@ class SetupCommand(BaseCommand):
|
||||
self.console.print(f"[red]Error installing Tiny🔥Torch: {e}[/red]")
|
||||
return False
|
||||
|
||||
# Register Jupyter kernel so notebooks use this Python environment
|
||||
self.console.print()
|
||||
self.console.print("[bold]Registering Jupyter kernel...[/bold]")
|
||||
try:
|
||||
result = subprocess.run([
|
||||
sys.executable, "-m", "ipykernel", "install",
|
||||
"--user",
|
||||
"--name", "tinytorch",
|
||||
"--display-name", "TinyTorch (Python 3)"
|
||||
], capture_output=True, text=True, timeout=60)
|
||||
|
||||
if result.returncode == 0:
|
||||
self.console.print("[green]✅ Jupyter kernel 'tinytorch' registered[/green]")
|
||||
self.console.print("[dim] Notebooks will use this Python environment[/dim]")
|
||||
else:
|
||||
self.console.print("[red]❌ Jupyter kernel registration failed[/red]")
|
||||
self.console.print(f"[dim] {result.stderr.strip()}[/dim]")
|
||||
self.console.print("[yellow] Fix: pip install ipykernel && "
|
||||
"python -m ipykernel install --user --name tinytorch[/yellow]")
|
||||
return False
|
||||
except FileNotFoundError:
|
||||
self.console.print("[red]❌ ipykernel not found — cannot register Jupyter kernel[/red]")
|
||||
self.console.print("[yellow] Fix: pip install ipykernel[/yellow]")
|
||||
return False
|
||||
except Exception as e:
|
||||
self.console.print(f"[red]❌ Kernel registration error: {e}[/red]")
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def create_virtual_environment(self, force: bool = False) -> bool:
|
||||
"""Create a virtual environment for Tiny🔥Torch development.
|
||||
|
||||
@@ -293,7 +324,9 @@ class SetupCommand(BaseCommand):
|
||||
self.console.print(f"✅ Virtual environment created at {venv_path}")
|
||||
|
||||
# Verify architecture
|
||||
venv_python = venv_path / "bin" / "python3"
|
||||
from ..core.virtual_env_manager import get_venv_bin_dir
|
||||
venv_bin = get_venv_bin_dir(venv_path)
|
||||
venv_python = venv_bin / ("python.exe" if sys.platform == "win32" else "python3")
|
||||
if venv_python.exists():
|
||||
arch_check = subprocess.run(
|
||||
[str(venv_python), "-c", "import platform; print(platform.machine())"],
|
||||
@@ -371,6 +404,7 @@ class SetupCommand(BaseCommand):
|
||||
("Python version (≥3.8)", self.check_python_version),
|
||||
("NumPy", self.check_numpy),
|
||||
("Jupyter", self.check_jupyter),
|
||||
("Jupyter kernel (tinytorch)", self.check_jupyter_kernel),
|
||||
("TinyTorch CLI", self.check_tinytorch_package)
|
||||
]
|
||||
|
||||
@@ -428,6 +462,17 @@ class SetupCommand(BaseCommand):
|
||||
except ImportError:
|
||||
return False
|
||||
|
||||
def check_jupyter_kernel(self) -> bool:
|
||||
"""Check if a TinyTorch Jupyter kernel is registered."""
|
||||
try:
|
||||
result = subprocess.run(
|
||||
[sys.executable, "-m", "jupyter", "kernelspec", "list"],
|
||||
capture_output=True, text=True, timeout=10
|
||||
)
|
||||
return result.returncode == 0 and "tinytorch" in result.stdout
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def check_tinytorch_package(self) -> bool:
|
||||
"""Check if Tiny🔥Torch package is installed (tito CLI)."""
|
||||
try:
|
||||
|
||||
@@ -4,6 +4,7 @@ Health command for TinyTorch CLI: environment health check and validation.
|
||||
|
||||
import sys
|
||||
import os
|
||||
import subprocess
|
||||
from argparse import ArgumentParser, Namespace
|
||||
from pathlib import Path
|
||||
from rich.panel import Panel
|
||||
@@ -32,6 +33,9 @@ class HealthCommand(BaseCommand):
|
||||
title="System Health", border_style="bright_green"))
|
||||
console.print()
|
||||
|
||||
# Track issues for summary
|
||||
issues = []
|
||||
|
||||
# Environment checks table - STATUS ONLY (no version numbers)
|
||||
env_table = Table(title="Environment Check", show_header=True, header_style="bold blue")
|
||||
env_table.add_column("Component", style="cyan", width=30)
|
||||
@@ -55,8 +59,10 @@ class HealthCommand(BaseCommand):
|
||||
venv_status = "[green]✅ OK[/green]"
|
||||
elif venv_exists:
|
||||
venv_status = "[yellow]⚠️ Not Activated[/yellow]"
|
||||
issues.append("Virtual environment exists but is not activated")
|
||||
else:
|
||||
venv_status = "[red]❌ Missing[/red]"
|
||||
issues.append("Virtual environment not found — run: tito setup")
|
||||
env_table.add_row("Virtual Environment", venv_status)
|
||||
|
||||
# Required dependencies (from requirements.txt)
|
||||
@@ -73,6 +79,20 @@ class HealthCommand(BaseCommand):
|
||||
env_table.add_row(display_name, "[green]✅ OK[/green]")
|
||||
except ImportError:
|
||||
env_table.add_row(display_name, "[red]❌ Missing[/red]")
|
||||
issues.append(f"{display_name} not installed")
|
||||
|
||||
# Workflow-critical dependencies (needed for module complete/export)
|
||||
workflow_deps = [
|
||||
('nbdev (export)', 'nbdev'),
|
||||
('ipykernel (Jupyter)', 'ipykernel'),
|
||||
]
|
||||
for display_name, import_name in workflow_deps:
|
||||
try:
|
||||
__import__(import_name)
|
||||
env_table.add_row(display_name, "[green]✅ OK[/green]")
|
||||
except ImportError:
|
||||
env_table.add_row(display_name, "[red]❌ Missing[/red]")
|
||||
issues.append(f"{display_name} not installed — run: pip install {import_name}")
|
||||
|
||||
# Optional dependencies (nice to have, not required for core workflow)
|
||||
optional_deps = [
|
||||
@@ -89,6 +109,115 @@ class HealthCommand(BaseCommand):
|
||||
console.print(env_table)
|
||||
console.print()
|
||||
|
||||
# ── Notebook Readiness checks ──
|
||||
# These diagnose the exact "ModuleNotFoundError" problem students hit
|
||||
nb_table = Table(title="Notebook Readiness", show_header=True, header_style="bold yellow")
|
||||
nb_table.add_column("Check", style="cyan", width=30)
|
||||
nb_table.add_column("Status", justify="center", width=15)
|
||||
nb_table.add_column("Detail", style="dim", width=35)
|
||||
|
||||
# 1. Can we import the tinytorch package at all?
|
||||
try:
|
||||
import tinytorch
|
||||
nb_table.add_row(
|
||||
"TinyTorch package",
|
||||
"[green]✅ OK[/green]",
|
||||
f"v{getattr(tinytorch, '__version__', 'unknown')}"
|
||||
)
|
||||
except ImportError as e:
|
||||
nb_table.add_row(
|
||||
"TinyTorch package",
|
||||
"[red]❌ Not importable[/red]",
|
||||
"run: pip install -e ."
|
||||
)
|
||||
issues.append("tinytorch package not importable — run: pip install -e .")
|
||||
|
||||
# 2. Does tinytorch/core/tensor.py exist? (the most common failure point)
|
||||
core_dir = self.config.project_root / "tinytorch" / "core"
|
||||
tensor_file = core_dir / "tensor.py"
|
||||
if tensor_file.exists():
|
||||
nb_table.add_row(
|
||||
"Core module files",
|
||||
"[green]✅ OK[/green]",
|
||||
f"{len(list(core_dir.glob('*.py')))} files in tinytorch/core/"
|
||||
)
|
||||
else:
|
||||
nb_table.add_row(
|
||||
"Core module files",
|
||||
"[red]❌ Missing[/red]",
|
||||
"tinytorch/core/tensor.py not found"
|
||||
)
|
||||
issues.append("tinytorch/core/tensor.py missing — package may be corrupted")
|
||||
|
||||
# 3. Can the Tensor class actually be imported?
|
||||
try:
|
||||
from tinytorch.core.tensor import Tensor
|
||||
if Tensor is not None:
|
||||
nb_table.add_row(
|
||||
"Tensor import",
|
||||
"[green]✅ OK[/green]",
|
||||
"from tinytorch.core.tensor import Tensor"
|
||||
)
|
||||
else:
|
||||
nb_table.add_row(
|
||||
"Tensor import",
|
||||
"[yellow]⚠️ None[/yellow]",
|
||||
"Module 01 may not be exported yet"
|
||||
)
|
||||
issues.append("Tensor is None — complete Module 01: tito module complete 01")
|
||||
except ImportError as e:
|
||||
nb_table.add_row(
|
||||
"Tensor import",
|
||||
"[red]❌ Failed[/red]",
|
||||
str(e)[:35]
|
||||
)
|
||||
issues.append(f"Cannot import Tensor: {e}")
|
||||
|
||||
# 4. Jupyter kernel check — does a kernel exist that points to this Python?
|
||||
kernel_status, kernel_detail = self._check_jupyter_kernel()
|
||||
nb_table.add_row("Jupyter kernel", kernel_status, kernel_detail)
|
||||
if "❌" in kernel_status or "⚠️" in kernel_status:
|
||||
issues.append(kernel_detail)
|
||||
|
||||
# 5. Check that this Python == the Jupyter kernel's Python
|
||||
# (catches the exact mismatch that causes ModuleNotFoundError in notebooks)
|
||||
kernel_python = self._get_kernel_python()
|
||||
if kernel_python:
|
||||
if os.path.realpath(kernel_python) == os.path.realpath(sys.executable):
|
||||
nb_table.add_row(
|
||||
"Kernel ↔ tito Python",
|
||||
"[green]✅ Match[/green]",
|
||||
"Same interpreter"
|
||||
)
|
||||
else:
|
||||
nb_table.add_row(
|
||||
"Kernel ↔ tito Python",
|
||||
"[red]❌ Mismatch[/red]",
|
||||
f"Kernel: {kernel_python}"
|
||||
)
|
||||
issues.append(
|
||||
f"Jupyter kernel uses a different Python than tito — "
|
||||
f"run: python -m ipykernel install --user --name tinytorch"
|
||||
)
|
||||
else:
|
||||
nb_table.add_row(
|
||||
"Kernel ↔ tito Python",
|
||||
"[dim]○ Skipped[/dim]",
|
||||
"No kernel to check"
|
||||
)
|
||||
|
||||
console.print(nb_table)
|
||||
console.print()
|
||||
|
||||
# ── Issues Summary ──
|
||||
if issues:
|
||||
console.print(Panel(
|
||||
"\n".join(f" • {issue}" for issue in issues),
|
||||
title=f"⚠️ {len(issues)} issue{'s' if len(issues) > 1 else ''} found",
|
||||
border_style="yellow"
|
||||
))
|
||||
console.print()
|
||||
|
||||
# Module structure table
|
||||
struct_table = Table(title="Module Structure", show_header=True, header_style="bold magenta")
|
||||
struct_table.add_column("Path", style="cyan", width=25)
|
||||
@@ -122,3 +251,53 @@ class HealthCommand(BaseCommand):
|
||||
info_cmd.add_arguments(info_args)
|
||||
info_args = info_args.parse_args([]) # Empty args for info
|
||||
return info_cmd.run(info_args)
|
||||
|
||||
def _check_jupyter_kernel(self):
|
||||
"""Check if a TinyTorch Jupyter kernel is registered."""
|
||||
try:
|
||||
result = subprocess.run(
|
||||
[sys.executable, "-m", "jupyter", "kernelspec", "list"],
|
||||
capture_output=True, text=True, timeout=10
|
||||
)
|
||||
if result.returncode == 0 and "tinytorch" in result.stdout:
|
||||
return "[green]✅ Registered[/green]", "tinytorch kernel found"
|
||||
elif result.returncode == 0:
|
||||
# Jupyter works but no tinytorch kernel
|
||||
return (
|
||||
"[yellow]⚠️ No tinytorch kernel[/yellow]",
|
||||
"run: python -m ipykernel install --user --name tinytorch"
|
||||
)
|
||||
else:
|
||||
return "[yellow]⚠️ Cannot list[/yellow]", "jupyter kernelspec list failed"
|
||||
except FileNotFoundError:
|
||||
return "[dim]○ Skipped[/dim]", "jupyter not installed"
|
||||
except Exception:
|
||||
return "[dim]○ Skipped[/dim]", "could not check"
|
||||
|
||||
def _get_kernel_python(self):
|
||||
"""Get the Python executable path used by the default or tinytorch Jupyter kernel."""
|
||||
try:
|
||||
import json
|
||||
|
||||
# Try tinytorch kernel first, then python3 default
|
||||
for kernel_name in ("tinytorch", "python3"):
|
||||
result = subprocess.run(
|
||||
[sys.executable, "-m", "jupyter", "kernelspec", "list", "--json"],
|
||||
capture_output=True, text=True, timeout=10
|
||||
)
|
||||
if result.returncode != 0:
|
||||
return None
|
||||
|
||||
data = json.loads(result.stdout)
|
||||
kernels = data.get("kernelspecs", {})
|
||||
if kernel_name in kernels:
|
||||
kernel_dir = kernels[kernel_name].get("resource_dir", "")
|
||||
kernel_json = Path(kernel_dir) / "kernel.json"
|
||||
if kernel_json.exists():
|
||||
spec = json.loads(kernel_json.read_text())
|
||||
argv = spec.get("argv", [])
|
||||
if argv:
|
||||
return argv[0] # First element is the Python path
|
||||
except Exception:
|
||||
pass
|
||||
return None
|
||||
|
||||
@@ -1,10 +1,17 @@
|
||||
import os, json
|
||||
import os, sys, json
|
||||
from pathlib import Path
|
||||
|
||||
DEFAULT_VENV = ".venv"
|
||||
CONFIG_FILE = ".tinyrc"
|
||||
|
||||
|
||||
def get_venv_bin_dir(venv_path: Path) -> Path:
|
||||
"""Return the bin directory for a venv (Scripts/ on Windows, bin/ on Unix)."""
|
||||
if sys.platform == "win32" or os.name == "nt":
|
||||
return venv_path / "Scripts"
|
||||
return venv_path / "bin"
|
||||
|
||||
|
||||
def get_venv_path() -> Path:
|
||||
"""
|
||||
Fetch venv in case users have a custom path
|
||||
|
||||
Reference in New Issue
Block a user