Skip to content

Commit a5052df

Browse files
AliciaFrameCopilot
andauthored
Add agent discovery symlinks, HelpOnErrorParser, PEP 723 deps, DefaultAzureCredential
* Add agent discovery symlinks for fine-tuning skill Add symlinks so coding agents (Copilot, Claude, Codex) can auto-discover the fine-tuning skill from their conventional paths: - .github/skills/azure-ai-fine-tuning -> ../../Skills - .claude/skills/azure-ai-fine-tuning -> ../../Skills - .agents/skills/azure-ai-fine-tuning -> ../../Skills This reuses the existing comprehensive skill (SFT, DPO, RFT with graders and tools, 8 scripts, 11 reference docs, 5 workflows) rather than creating a separate thinner skill. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Add HelpOnErrorParser, PEP 723 deps, DefaultAzureCredential, and AI Agent Skills docs Adopts three patterns from PR #27: 1. HelpOnErrorParser in common.py — prints full --help on invalid args 2. PEP 723 inline script metadata (# /// script) on all scripts — enables 'uv run scripts/submit_training.py' with auto-installed deps 3. DefaultAzureCredential fallback in get_clients() — works without API key when az CLI or Managed Identity is available Also adds AI Agent Skills section to README with usage instructions for Copilot (VS Code + CLI), Claude Code, and Codex agents. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix: scripts now use HelpOnErrorParser, fix credential fallback bug - All 7 scripts now import and use HelpOnErrorParser from common.py (previously added the class but scripts still used argparse.ArgumentParser) - Fix bug in DefaultAzureCredential fallback: when base_url is set but api_key is None and credential fails, no longer creates client with api_key=None (which would fail silently on first API call) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix: DPO system message bug, cross-dir import path, dedup imports - convert_dataset.py: Fix DPO generation to include system messages when generating non-preferred responses from base model (was generating without system prompt but recording it in the DPO input — distributional mismatch) - All scripts: Add sys.path.insert for common.py import to work when scripts are run from outside the scripts/ directory - Clean up duplicate os/sys imports Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 5028503 commit a5052df

12 files changed

Lines changed: 150 additions & 16 deletions
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
../../Skills
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
../../Skills
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
../../Skills

README.md

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ This repository contains **12 end-to-end demos** and **sample datasets** for fin
77
- [Quick Start](#-quick-start)
88
- [Demos](#-demos)
99
- [Sample Datasets](#-sample-datasets)
10+
- [AI Agent Skills](#-ai-agent-skills)
1011
- [Prerequisites](#-prerequisites)
1112
- [Contributing](#contributing)
1213

@@ -66,6 +67,52 @@ Ready-to-use datasets for testing fine-tuning techniques in the **[Sample_Datase
6667
6768
---
6869

70+
## 🤖 AI Agent Skills
71+
72+
This repo includes a fine-tuning skill that coding agents can auto-discover and use to help you submit, monitor, and evaluate fine-tuning jobs.
73+
74+
| Agent | Skill Path | Auto-discovery |
75+
|-------|-----------|----------------|
76+
| **GitHub Copilot** (VS Code / CLI) | [.github/skills/azure-ai-fine-tuning](.github/skills/azure-ai-fine-tuning) | ✅ Automatic |
77+
| **Claude Code** | [.claude/skills/azure-ai-fine-tuning](.claude/skills/azure-ai-fine-tuning) | ✅ Automatic |
78+
| **Codex / other agents** | [.agents/skills/azure-ai-fine-tuning](.agents/skills/azure-ai-fine-tuning) | ✅ Automatic |
79+
80+
All three paths are symlinks to the canonical skill at **[Skills/](Skills/)**, which includes:
81+
- **SKILL.md** — Agent instructions covering SFT, DPO, and RFT workflows
82+
- **8 scripts** — submit, monitor, deploy, evaluate, validate, score, convert, distill
83+
- **11 reference docs** — hyperparameters, dataset formats, agentic RFT, cost management, and more
84+
- **5 guided workflows** — full pipeline, dataset creation, iterative training, diagnosis
85+
- **Sample data** — SFT, DPO, and RFT example JSONL files
86+
87+
### Using with GitHub Copilot (VS Code)
88+
89+
1. Open this repo in VS Code with Copilot Chat enabled.
90+
2. Ask a fine-tuning task (e.g., *"help me submit an SFT job with my dataset"*).
91+
3. Copilot auto-discovers the skill from `.github/skills/` and follows the workflow.
92+
93+
### Using with Copilot CLI
94+
95+
```bash
96+
cd /path/to/this/repo
97+
copilot
98+
# Then ask: "Submit an SFT fine-tuning job with my training data"
99+
```
100+
101+
### Using with Claude Code
102+
103+
```bash
104+
cd /path/to/this/repo
105+
claude
106+
# Then ask: "Fine-tune gpt-4.1-mini on my dataset"
107+
```
108+
109+
Scripts support `uv` for zero-setup execution (PEP 723 inline dependencies):
110+
```bash
111+
uv run Skills/scripts/submit_training.py --help
112+
```
113+
114+
---
115+
69116
## ✅ Prerequisites
70117

71118
Before running any demo, ensure you have:

Skills/scripts/check_training.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,10 @@
1+
# /// script
2+
# dependencies = [
3+
# "openai>=1.0",
4+
# "azure-identity",
5+
# "azure-ai-projects",
6+
# ]
7+
# ///
18
"""
29
check_training.py — Analyze training curves, detect overfitting, list checkpoints.
310
@@ -13,6 +20,8 @@
1320
import json
1421
import os
1522
import sys
23+
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
24+
from common import HelpOnErrorParser
1625

1726
import openai
1827

@@ -160,7 +169,7 @@ def analyze_job(client, job_id, download_csv=None):
160169

161170

162171
def main():
163-
parser = argparse.ArgumentParser(description="Analyze fine-tuning training curves")
172+
parser = HelpOnErrorParser(description="Analyze fine-tuning training curves")
164173
parser.add_argument("--base-url", default=os.environ.get("OPENAI_BASE_URL"),
165174
help="Project /v1/ URL (preferred). Uses openai.OpenAI().")
166175
parser.add_argument("--endpoint", default=os.environ.get("AZURE_OPENAI_ENDPOINT"),

Skills/scripts/common.py

Lines changed: 34 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
"""
22
common.py — Shared Azure AI Foundry authentication and client setup.
33
4-
Adapted from foundry-ft agent's common.py, with our enhancements:
5-
- Supports /v1/ project endpoint (preferred) and Azure endpoint (fallback)
6-
- REST API fallback for OSS models
7-
- No .env dependency — works with az CLI tokens directly
4+
Supports three connection methods in order of preference:
5+
1. /v1/ project endpoint (simplest, preferred)
6+
2. Foundry SDK with DefaultAzureCredential (no API key needed, cloud-native)
7+
3. Azure OpenAI endpoint (classic)
88
99
Usage:
1010
from common import get_clients, upload_file
@@ -13,17 +13,30 @@
1313
clients = get_clients(base_url="https://<resource>.services.ai.azure.com/api/projects/<project>/openai/v1/",
1414
api_key="KEY")
1515
16-
# Method 2: Foundry SDK
16+
# Method 2: Foundry SDK (DefaultAzureCredential — no API key needed)
1717
clients = get_clients(project_endpoint="https://<resource>.services.ai.azure.com/api/projects/<project>")
1818
1919
# Method 3: Azure OpenAI endpoint
2020
clients = get_clients(azure_endpoint="https://<resource>.openai.azure.com",
2121
api_key="KEY")
2222
"""
23+
import argparse
2324
import os
2425
import sys
2526

2627

28+
class HelpOnErrorParser(argparse.ArgumentParser):
29+
"""ArgumentParser that prints full help when arguments are invalid.
30+
31+
Standard ArgumentParser only prints a one-line usage summary on error,
32+
which isn't helpful for first-time users. This prints the full --help.
33+
"""
34+
35+
def error(self, message):
36+
self.print_help(sys.stderr)
37+
self.exit(2, f"\nerror: {message}\n")
38+
39+
2740
def get_clients(base_url=None, azure_endpoint=None, project_endpoint=None, api_key=None):
2841
"""Initialize and return OpenAI-compatible client.
2942
@@ -40,9 +53,22 @@ def get_clients(base_url=None, azure_endpoint=None, project_endpoint=None, api_k
4053

4154
if base_url:
4255
import openai
43-
client = openai.OpenAI(base_url=base_url, api_key=api_key)
44-
print(f"✅ Connected via /v1/ project endpoint")
45-
return client, "project-v1"
56+
# If no API key, try DefaultAzureCredential for token-based auth
57+
if not api_key:
58+
try:
59+
from azure.identity import DefaultAzureCredential
60+
credential = DefaultAzureCredential()
61+
token = credential.get_token("https://cognitiveservices.azure.com/.default")
62+
client = openai.OpenAI(base_url=base_url, api_key=token.token)
63+
print(f"✅ Connected via /v1/ project endpoint (DefaultAzureCredential)")
64+
return client, "project-v1-aad"
65+
except Exception as e:
66+
print(f"⚠️ No API key and DefaultAzureCredential failed: {e}")
67+
# Fall through to Method 2/3
68+
else:
69+
client = openai.OpenAI(base_url=base_url, api_key=api_key)
70+
print(f"✅ Connected via /v1/ project endpoint")
71+
return client, "project-v1"
4672

4773
# Method 2: Foundry SDK
4874
project_endpoint = project_endpoint or os.environ.get("AZURE_AI_PROJECT_ENDPOINT")

Skills/scripts/convert_dataset.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
# /// script
2+
# dependencies = [
3+
# "openai>=1.0",
4+
# ]
5+
# ///
16
"""
27
convert_dataset.py — Convert between SFT, DPO, and RFT dataset formats.
38
@@ -22,6 +27,8 @@
2227
import os
2328
import sys
2429
import time
30+
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
31+
from common import HelpOnErrorParser
2532

2633

2734
def parquet_to_sft(input_path, output_path, user_col, assistant_col, system_prompt=None):
@@ -92,7 +99,7 @@ def sft_to_dpo(input_path, output_path, endpoint, api_key, base_model):
9299

93100
# Generate a non-preferred response from the base model
94101
try:
95-
gen_msgs = [user_msg]
102+
gen_msgs = system_msgs + [user_msg]
96103
resp = client.chat.completions.create(
97104
model=base_model,
98105
messages=gen_msgs,
@@ -154,7 +161,7 @@ def dpo_to_sft(input_path, output_path, system_prompt=None):
154161

155162

156163
def main():
157-
parser = argparse.ArgumentParser(description="Convert between fine-tuning dataset formats")
164+
parser = HelpOnErrorParser(description="Convert between fine-tuning dataset formats")
158165
parser.add_argument("--input", required=True, help="Input file path")
159166
parser.add_argument("--output", required=True, help="Output file path")
160167
parser.add_argument("--format", required=True,

Skills/scripts/deploy_model.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
# /// script
2+
# dependencies = [
3+
# "openai>=1.0",
4+
# "azure-identity",
5+
# ]
6+
# ///
17
"""
28
deploy_model.py — Deploy fine-tuned models on Azure AI Foundry via ARM REST API.
39
@@ -16,6 +22,8 @@
1622
import subprocess
1723
import sys
1824
import time
25+
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
26+
from common import HelpOnErrorParser
1927

2028
import requests
2129

@@ -167,7 +175,7 @@ def list_deployments(sub, rg, account):
167175

168176

169177
def main():
170-
parser = argparse.ArgumentParser(description="Deploy fine-tuned models on Azure AI Foundry")
178+
parser = HelpOnErrorParser(description="Deploy fine-tuned models on Azure AI Foundry")
171179
parser.add_argument("--sub", default=DEFAULT_SUB, help="Azure subscription ID")
172180
parser.add_argument("--rg", default=DEFAULT_RG, help="Resource group")
173181
parser.add_argument("--account", default=DEFAULT_ACCOUNT, help="Cognitive Services account")

Skills/scripts/evaluate_model.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
# /// script
2+
# dependencies = [
3+
# "openai>=1.0",
4+
# "azure-identity",
5+
# ]
6+
# ///
17
"""
28
evaluate_model.py — Custom 2-dimension LLM judge evaluator for fine-tuned models.
39
@@ -30,6 +36,8 @@
3036
import sys
3137
import time
3238
from concurrent.futures import ThreadPoolExecutor, as_completed
39+
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
40+
from common import HelpOnErrorParser
3341

3442
import openai
3543

@@ -149,7 +157,7 @@ def grade_response(judge_client, judge_model, prompt, reference, output, max_ret
149157

150158

151159
def main():
152-
parser = argparse.ArgumentParser(description="Evaluate a fine-tuned model with LLM judge")
160+
parser = HelpOnErrorParser(description="Evaluate a fine-tuned model with LLM judge")
153161
parser.add_argument("--base-url", default=os.environ.get("OPENAI_BASE_URL"),
154162
help="Project /v1/ URL (preferred). Uses openai.OpenAI().")
155163
parser.add_argument("--endpoint", default=os.environ.get("AZURE_OPENAI_ENDPOINT"),

Skills/scripts/generate_distillation_data.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
# /// script
2+
# dependencies = [
3+
# "openai>=1.0",
4+
# "azure-identity",
5+
# ]
6+
# ///
17
"""
28
generate_distillation_data.py — Generate training data from a teacher model for distillation.
39
@@ -31,6 +37,8 @@
3137
import re
3238
import sys
3339
import time
40+
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
41+
from common import HelpOnErrorParser
3442

3543
import openai
3644

@@ -123,7 +131,7 @@ def grade_output(client, judge_model, output, retries=3):
123131

124132

125133
def main():
126-
parser = argparse.ArgumentParser(description="Generate distillation training data from a teacher model")
134+
parser = HelpOnErrorParser(description="Generate distillation training data from a teacher model")
127135
parser.add_argument("--endpoint", default=os.environ.get("AZURE_OPENAI_ENDPOINT"))
128136
parser.add_argument("--api-key", default=os.environ.get("AZURE_OPENAI_API_KEY"))
129137
parser.add_argument("--teacher", required=True, help="Teacher model deployment name")

0 commit comments

Comments
 (0)