Agent Skills Pipeline v1.0
8-stage pipeline: Scout → Filter → Reader → Extractor → Score → Generator → Reviewer → Publisher - Scout: GitHub search with token auth + rate limit retry - Filter: Deterministic rules (language, stars, age, keywords) - Reader: Incremental context loading (README → docs → examples → code) - Extractor: LLM workflow extraction with JSON retry - Score: Rule-based evaluation (no LLM) - Generator: Standardized Hermes Skill format - Reviewer: Independent LLM review (separate from generator) - Publisher: Branch + PR to Gitea First run: 5 repos discovered, 0 extracted (correct — all frameworks, no workflows)
This commit is contained in:
@@ -0,0 +1,164 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Agent Skills Pipeline Runner — full 8-stage pipeline."""
|
||||
import yaml
|
||||
import json
|
||||
import sys
|
||||
import os
|
||||
import datetime
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
from pipeline.scout import scout
|
||||
from pipeline.filter import filter_repos
|
||||
from pipeline.reader import read_repo
|
||||
from pipeline.extractor import extract_workflow
|
||||
from pipeline.scorer import score_workflow
|
||||
from pipeline.generator import generate_skill
|
||||
from pipeline.reviewer import review_skill
|
||||
from pipeline.publisher import publish_skill
|
||||
|
||||
def load_config():
|
||||
config_path = os.path.join(os.path.dirname(__file__), "config", "settings.yaml")
|
||||
with open(config_path) as f:
|
||||
return yaml.safe_load(f)
|
||||
|
||||
def main():
|
||||
config = load_config()
|
||||
runs_dir = os.path.join(os.path.dirname(__file__), "runs")
|
||||
os.makedirs(runs_dir, exist_ok=True)
|
||||
|
||||
run_id = datetime.datetime.now().strftime("%Y%m%d-%H%M%S")
|
||||
run_log = {"run_id": run_id, "started_at": datetime.datetime.now().isoformat(), "stages": {}}
|
||||
|
||||
print("=" * 60)
|
||||
print("Agent Skills Pipeline — Run", run_id)
|
||||
print("=" * 60)
|
||||
|
||||
# --- Stage 1: Scout ---
|
||||
print("\n[1/8] Scout — Discovering repos from GitHub...")
|
||||
state = {}
|
||||
scout_result = scout(config, state)
|
||||
|
||||
if scout_result.get("status") in ("COOLDOWN", "RATE_LIMITED"):
|
||||
print(f" ⏸ {scout_result.get('message', scout_result.get('status'))}")
|
||||
return
|
||||
|
||||
repos = scout_result.get("repos", [])
|
||||
print(f" Found {scout_result.get('count', 0)} repos")
|
||||
run_log["stages"]["scout"] = {"count": scout_result.get("count", 0)}
|
||||
|
||||
if not repos:
|
||||
print(" No repos found. Exiting.")
|
||||
return
|
||||
|
||||
# --- Stage 2: Filter ---
|
||||
print("\n[2/8] Filter — Applying deterministic rules...")
|
||||
filter_result = filter_repos(repos, config)
|
||||
kept = filter_result.get("kept", [])
|
||||
print(f" Kept: {filter_result.get('kept_count', 0)} | Rejected: {filter_result.get('rejected_count', 0)}")
|
||||
run_log["stages"]["filter"] = {"kept": filter_result.get("kept_count", 0), "rejected": filter_result.get("rejected_count", 0)}
|
||||
|
||||
for rej in filter_result.get("rejected", [])[:3]:
|
||||
print(f" ✗ {rej['repo'].get('name', '?')}: {', '.join(rej['reasons'])}")
|
||||
|
||||
if not kept:
|
||||
print(" All repos filtered out. Exiting.")
|
||||
return
|
||||
|
||||
# --- Stages 3-8: Process each kept repo ---
|
||||
print(f"\n[3-8/8] Processing {len(kept)} repos through pipeline...")
|
||||
results = {"extracted": 0, "scored": 0, "generated": 0, "reviewed": 0, "published": 0}
|
||||
|
||||
for i, repo in enumerate(kept[:5]): # Cap at 5 per run
|
||||
repo_name = repo.get("full_name", repo.get("name", "?"))
|
||||
repo_url = repo.get("url", "")
|
||||
print(f"\n ── Repo {i+1}/{min(len(kept), 5)}: {repo_name} ──")
|
||||
|
||||
# Stage 3: Reader
|
||||
print(f" [3/8] Reader — Loading context...")
|
||||
reader_output = read_repo(repo.get("clone_url", repo_url), config)
|
||||
|
||||
if reader_output.get("status") == "INSUFFICIENT":
|
||||
print(f" ⏸ Insufficient context: {reader_output.get('decision_reason', '')}")
|
||||
continue
|
||||
|
||||
print(f" Loaded {len(reader_output.get('context_loaded', []))} files")
|
||||
|
||||
# Stage 4: Extractor
|
||||
print(f" [4/8] Extractor — Looking for reusable workflow...")
|
||||
extract_output = extract_workflow(reader_output, config)
|
||||
|
||||
if extract_output.get("status") != "EXTRACTED":
|
||||
print(f" ✗ No workflow: {extract_output.get('reason', extract_output.get('status', ''))}")
|
||||
continue
|
||||
|
||||
workflow = extract_output.get("workflow", {})
|
||||
print(f" ✓ Extracted: {workflow.get('skill_name', '?')} (confidence: {workflow.get('confidence', 0)})")
|
||||
results["extracted"] += 1
|
||||
|
||||
# Stage 5: Scorer
|
||||
print(f" [5/8] Scorer — Evaluating...")
|
||||
extract_output["reader_output"] = reader_output
|
||||
score_output = score_workflow(extract_output, config)
|
||||
|
||||
if score_output.get("decision") != "PASS":
|
||||
print(f" ✗ Score {score_output.get('score', 0)} < {score_output.get('min_score', 0.85)}")
|
||||
continue
|
||||
|
||||
print(f" ✓ Score: {score_output.get('score', 0)} (passed)")
|
||||
results["scored"] += 1
|
||||
|
||||
# Stage 6: Generator
|
||||
print(f" [6/8] Generator — Building Skill package...")
|
||||
gen_output = generate_skill(score_output, config)
|
||||
|
||||
if gen_output.get("status") != "GENERATED":
|
||||
print(f" ✗ Generation blocked: {gen_output.get('reason', '')}")
|
||||
continue
|
||||
|
||||
print(f" ✓ Generated: {gen_output.get('skill_name', '?')} ({len(gen_output.get('files', {}))} files)")
|
||||
results["generated"] += 1
|
||||
|
||||
# Stage 7: Reviewer
|
||||
print(f" [7/8] Reviewer — LLM review...")
|
||||
review_output = review_skill(gen_output, config)
|
||||
|
||||
if review_output.get("status") != "APPROVED":
|
||||
print(f" ✗ Review: {review_output.get('status', '?')} — {review_output.get('reason', '')[:100]}")
|
||||
continue
|
||||
|
||||
print(f" ✓ Approved (confidence: {review_output.get('confidence', 0)})")
|
||||
results["reviewed"] += 1
|
||||
|
||||
# Stage 8: Publisher
|
||||
print(f" [8/8] Publisher — Creating PR...")
|
||||
publish_output = publish_skill(review_output, config)
|
||||
|
||||
if publish_output.get("status") == "PUBLISHED":
|
||||
print(f" ✓ Published! PR: {publish_output.get('pr_url', '')}")
|
||||
results["published"] += 1
|
||||
else:
|
||||
print(f" ! {publish_output.get('status', '?')}: {publish_output.get('message', publish_output.get('error', ''))[:100]}")
|
||||
|
||||
# --- Summary ---
|
||||
print("\n" + "=" * 60)
|
||||
print("PIPELINE COMPLETE")
|
||||
print("=" * 60)
|
||||
print(f" Scout: {scout_result.get('count', 0)} discovered")
|
||||
print(f" Filter: {filter_result.get('kept_count', 0)} kept / {filter_result.get('rejected_count', 0)} rejected")
|
||||
print(f" Extracted: {results['extracted']}")
|
||||
print(f" Scored: {results['scored']}")
|
||||
print(f" Generated: {results['generated']}")
|
||||
print(f" Reviewed: {results['reviewed']}")
|
||||
print(f" Published: {results['published']}")
|
||||
|
||||
# Save run log
|
||||
run_log["results"] = results
|
||||
run_log["ended_at"] = datetime.datetime.now().isoformat()
|
||||
log_path = os.path.join(runs_dir, f"{run_id}.json")
|
||||
with open(log_path, 'w') as f:
|
||||
json.dump(run_log, f, indent=2)
|
||||
print(f"\n Run log: {log_path}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user