1c6c1952e8
Regressions fixed (aa916de simplification):
- firmware/platformio.ini: restore esp32s3_waveshare (pioarduino platform,
lib_deps: ArduinoJson/ESP32_Display_Panel/ESP32-audioI2S/IO_Expander,
BOARD_HAS_PSRAM, I2S pins), restore esp32s3_qemu (extends waveshare +
QEMU_BUILD), fix default_envs=esp32s3_waveshare, keep build_dir=/tmp/kl_pio_build
- .github/workflows/ci.yml: restore firmware-native (112 Unity tests),
firmware-build (esp32s3_waveshare artifact), firmware-sim (Wokwi gated),
hardware-export (KiCad ERC + SVG/PDF/netlist + KiBot + compliance)
- .gitignore: add .kibot-venv/ and .pio-venv/ (prevent committing venvs)
Dataset (HuggingFace clemsail/kill-life-embedded-qa v2):
- generate_hf_dataset.py: rag_query timeout 30s->120s (LLM takes ~45s),
rag_search timeout 15s->30s; resolves intermittent server-busy failures
- 30 entries (10+10+5+5) -- 100% coverage vs 21/40 previously
Firmware tests (112/112):
- 4 suites: test_basic (39), test_modules (32), test_radio_state (26),
test_wifi_state (15); test_logic.cpp now a stub (content moved to test_basic)
Hardware:
- esp32_minimal.kicad_pcb, esp32s3_enclosure.FCStd/.step, gen_pcb.py,
gen_enclosure.py, REGISTRY.md, ERC reports for all design blocks
MCP tools:
- apify_mcp.py: +5 Kill_LIFE tools (fetch_espressif_docs, fetch_kicad_library_info,
fetch_platformio_registry, ingest_to_rag, get_runtime_info)
- mcp_runtime_status.py: fix classify_overall -- accept_degraded respected for
failed checks, task annotations, optional_degraded logic cleaned up
- deploy/cad/docker-compose.yml: path mascarade->mascarade-main
Specs & docs:
- docs/plans/TODO_2026-03-26.md, TODO_2026-03-27.md
- ai-agentic-embedded-base/specs: arch, tasks, intake, spec updates
- docs/playbooks/kicad_happy_hw_bom_forge.md
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
178 lines
6.9 KiB
Python
178 lines
6.9 KiB
Python
#!/usr/bin/env python3
|
|
"""Generate HuggingFace dataset entries from Kill_LIFE RAG collections.
|
|
|
|
Queries the mascarade RAG pipeline to extract Q&A pairs for fine-tuning
|
|
datasets. Outputs JSONL format compatible with HuggingFace datasets.
|
|
|
|
Usage:
|
|
python3 tools/generate_hf_dataset.py --collection kb-kicad --output datasets/kicad_qa.jsonl
|
|
python3 tools/generate_hf_dataset.py --collection kb-firmware --output datasets/firmware_qa.jsonl
|
|
python3 tools/generate_hf_dataset.py --all --output-dir datasets/
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
import os
|
|
import sys
|
|
from datetime import datetime, timezone
|
|
from pathlib import Path
|
|
|
|
import requests
|
|
|
|
MASCARADE_URL = os.environ.get("MASCARADE_URL", "http://localhost:8100")
|
|
|
|
# Seed questions per collection for dataset generation
|
|
SEED_QUESTIONS = {
|
|
"kb-kicad": [
|
|
"What components are in the Kill_LIFE ESP32-S3 minimal board?",
|
|
"How is the power supply designed for the ESP32-S3 board?",
|
|
"What are the KiCad ERC rules for Kill_LIFE schematics?",
|
|
"What is the pin assignment for the ESP32-S3-WROOM-1?",
|
|
"How do the reusable design blocks connect via net labels?",
|
|
"What decoupling capacitors are used on the +3V3 rail?",
|
|
"How does the USB-C power input work in the Kill_LIFE board?",
|
|
"What is the AMS1117-3.3 LDO configuration?",
|
|
"How was the SPICE simulation for the power LDO validated?",
|
|
"What ferrite bead is used for EMI filtering?",
|
|
],
|
|
"kb-firmware": [
|
|
"How does the Kill_LIFE firmware voice pipeline work?",
|
|
"What is the OTA update protocol?",
|
|
"How does the WiFi captive portal provisioning work?",
|
|
"What are the I2S pin assignments for audio?",
|
|
"How is the radio player implemented?",
|
|
"What pure functions are available for native testing?",
|
|
"How does the button handling work (short/long press)?",
|
|
"What is the WiFi scanner architecture?",
|
|
"How does the firmware communicate with the Mascarade backend?",
|
|
"What media modes does the RadioPlayer support?",
|
|
],
|
|
"kb-spice": [
|
|
"What does the LDO power simulation show?",
|
|
"How is the WiFi TX burst modeled in SPICE?",
|
|
"What is the voltage droop under load?",
|
|
"How are decoupling capacitors modeled?",
|
|
"What is the SPICE model for the AMS1117?",
|
|
],
|
|
"kb-components": [
|
|
"What is the ESP32-S3-WROOM-1-N16R8 module?",
|
|
"What are the specifications of the AMS1117-3.3?",
|
|
"What is the ICS-43434 MEMS microphone?",
|
|
"What is the PCM5101 I2S DAC?",
|
|
"What USB-C connector is used?",
|
|
],
|
|
}
|
|
|
|
|
|
def rag_query(collection: str, question: str) -> dict | None:
|
|
"""Query RAG pipeline and return the response (LLM synthesis, ~45-90s)."""
|
|
try:
|
|
resp = requests.post(
|
|
f"{MASCARADE_URL}/v1/rag/query",
|
|
json={"query": question, "collection": collection, "retrieve_k": 5, "rerank_top_k": 3},
|
|
timeout=120,
|
|
)
|
|
if resp.status_code == 200:
|
|
return resp.json()
|
|
except requests.RequestException:
|
|
pass
|
|
return None
|
|
|
|
|
|
def rag_search(collection: str, question: str) -> list[str]:
|
|
"""Search RAG and return matching text chunks."""
|
|
try:
|
|
resp = requests.post(
|
|
f"{MASCARADE_URL}/v1/rag/search",
|
|
json={"query": question, "collection": collection, "limit": 3},
|
|
timeout=30,
|
|
)
|
|
if resp.status_code == 200:
|
|
data = resp.json()
|
|
return [r["text"] for r in data.get("results", []) if r.get("text")]
|
|
except requests.RequestException:
|
|
pass
|
|
return []
|
|
|
|
|
|
def generate_entries(collection: str, questions: list[str]) -> list[dict]:
|
|
"""Generate dataset entries from RAG queries."""
|
|
entries = []
|
|
for q in questions:
|
|
# Try full query first (with LLM synthesis)
|
|
result = rag_query(collection, q)
|
|
if result and result.get("answer"):
|
|
entries.append({
|
|
"instruction": q,
|
|
"input": "",
|
|
"output": result["answer"],
|
|
"source": f"rag:{collection}",
|
|
"generated_at": datetime.now(timezone.utc).isoformat(),
|
|
})
|
|
print(f" [query] {q[:60]}... → {len(result['answer'])} chars")
|
|
continue
|
|
|
|
# Fallback: search + format context as answer
|
|
chunks = rag_search(collection, q)
|
|
if chunks:
|
|
context = "\n\n".join(chunks[:3])
|
|
entries.append({
|
|
"instruction": q,
|
|
"input": "",
|
|
"output": context,
|
|
"source": f"rag-search:{collection}",
|
|
"generated_at": datetime.now(timezone.utc).isoformat(),
|
|
})
|
|
print(f" [search] {q[:60]}... → {len(context)} chars")
|
|
else:
|
|
print(f" [skip] {q[:60]}... → no results")
|
|
|
|
return entries
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description="Generate HF dataset from RAG")
|
|
parser.add_argument("--collection", "-c", help="RAG collection name")
|
|
parser.add_argument("--output", "-o", help="Output JSONL file path")
|
|
parser.add_argument("--all", action="store_true", help="Generate for all collections")
|
|
parser.add_argument("--output-dir", "-d", default="datasets", help="Output dir for --all")
|
|
args = parser.parse_args()
|
|
|
|
if args.all:
|
|
out_dir = Path(args.output_dir)
|
|
out_dir.mkdir(parents=True, exist_ok=True)
|
|
total = 0
|
|
for coll, questions in SEED_QUESTIONS.items():
|
|
print(f"\n=== Collection: {coll} ({len(questions)} questions) ===")
|
|
entries = generate_entries(coll, questions)
|
|
out_path = out_dir / f"{coll.replace('-', '_')}_qa.jsonl"
|
|
with open(out_path, "w") as f:
|
|
for e in entries:
|
|
f.write(json.dumps(e, ensure_ascii=False) + "\n")
|
|
print(f" → {len(entries)} entries written to {out_path}")
|
|
total += len(entries)
|
|
print(f"\nTotal: {total} entries across {len(SEED_QUESTIONS)} collections")
|
|
elif args.collection:
|
|
questions = SEED_QUESTIONS.get(args.collection, [])
|
|
if not questions:
|
|
print(f"No seed questions for collection '{args.collection}'")
|
|
print(f"Available: {list(SEED_QUESTIONS.keys())}")
|
|
sys.exit(1)
|
|
print(f"Collection: {args.collection} ({len(questions)} questions)")
|
|
entries = generate_entries(args.collection, questions)
|
|
out_path = Path(args.output or f"datasets/{args.collection.replace('-','_')}_qa.jsonl")
|
|
out_path.parent.mkdir(parents=True, exist_ok=True)
|
|
with open(out_path, "w") as f:
|
|
for e in entries:
|
|
f.write(json.dumps(e, ensure_ascii=False) + "\n")
|
|
print(f"\n{len(entries)} entries written to {out_path}")
|
|
else:
|
|
parser.print_help()
|
|
sys.exit(1)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|