Files
agent-skills/pipeline/extractor.py
T
VPS admin 8da8d703da 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)
2026-08-05 05:51:06 +00:00

169 lines
5.3 KiB
Python

"""Stage 4: Workflow Extractor — Extract reusable workflow from repo context."""
import json
import os
import requests
import re
def call_llm(prompt, config):
"""Call the configured LLM for extraction."""
llm_config = config.get("llm", {})
base_url = llm_config.get("base_url", "http://100.64.0.2:8083/v1")
model = llm_config.get("model", "")
api_key = llm_config.get("api_key", "")
max_tokens = llm_config.get("max_tokens", 8000)
headers = {
"Content-Type": "application/json",
}
if api_key:
headers["Authorization"] = f"Bearer {api_key}"
payload = {
"model": model,
"messages": [
{"role": "system", "content": prompt},
],
"max_tokens": max_tokens,
"temperature": 0.1,
}
try:
resp = requests.post(f"{base_url}/v1/chat/completions", json=payload, headers=headers, timeout=120)
if resp.status_code == 200:
data = resp.json()
return data["choices"][0]["message"]["content"]
else:
return f"LLM error: {resp.status_code} {resp.text[:200]}"
except Exception as e:
return f"LLM error: {str(e)}"
def extract_workflow(reader_output, config):
"""
Attempt to extract a reusable workflow from the repo context.
Returns structured workflow or rejection.
"""
repo = reader_output.get("repository", "")
content_sections = reader_output.get("content", {})
# Build context for LLM
context_parts = []
for path, content in content_sections.items():
context_parts.append(f"--- {path} ---\n{content[:15000]}")
context = "\n\n".join(context_parts)
prompt = f"""You are a workflow extractor. Your job is to analyze a GitHub repository and determine if it contains a reusable AI workflow or pattern that another agent could learn from.
If the repository contains a reusable workflow, extract it into this exact JSON structure:
{{
"has_workflow": true,
"skill_name": "short-descriptive-name",
"goal": "One sentence: what this workflow accomplishes",
"inputs": ["Input 1", "Input 2"],
"steps": ["Step 1", "Step 2", "Step 3"],
"outputs": ["Output 1", "Output 2"],
"failure_modes": ["What can go wrong"],
"confidence": 0.95,
"reusable": true,
"general_purpose": true,
"explanation": "Why this is reusable and general-purpose"
}}
If the repository does NOT contain a reusable workflow, return:
{{
"has_workflow": false,
"reason": "Why no reusable workflow was found"
}}
Criteria for a reusable workflow:
- It describes a process or pattern, not just a tool or library
- It has clear inputs, steps, and outputs
- It could be applied to different contexts outside this specific repo
- It has at least 3 distinct steps
- It solves a real problem, not a toy example
Repository: {repo}
Repository context:
{context[:30000]}
Return ONLY valid JSON. No markdown, no explanation outside the JSON."""
result_text = call_llm(prompt, config)
# Parse JSON from response — try multiple strategies
workflow = None
for attempt in range(3):
try:
# Strip markdown code blocks if present
cleaned = result_text.strip()
if cleaned.startswith("```"):
cleaned = cleaned.split("```")[1]
if cleaned.startswith("json"):
cleaned = cleaned[4:]
cleaned = cleaned.rstrip("```")
cleaned = cleaned.strip()
# Try to find JSON object in text
json_match = re.search(r'\{.*\}', cleaned, re.DOTALL)
if json_match:
cleaned = json_match.group()
workflow = json.loads(cleaned)
break
except json.JSONDecodeError:
if attempt < 2:
# Retry with a simpler prompt
prompt = f"""Extract a reusable workflow from this repo as JSON. Return ONLY valid JSON.
Repo: {repo}
Context (first 10000 chars): {context[:10000]}
Format:
{{"has_workflow": true/false, "skill_name": "...", "goal": "...", "inputs": [...], "steps": [...], "outputs": [...], "failure_modes": [...], "confidence": 0-1, "reusable": true/false, "general_purpose": true/false, "explanation": "..."}}
If no reusable workflow: {{"has_workflow": false, "reason": "..."}}
Return ONLY JSON. No markdown."""
result_text = call_llm(prompt, config)
else:
break
if workflow is None:
return {
"status": "PARSE_ERROR",
"raw": result_text[:500],
"repository": repo,
}
if not workflow.get("has_workflow", False):
return {
"status": "NO_WORKFLOW",
"reason": workflow.get("reason", "Extractor determined no reusable workflow"),
"repository": repo,
}
# Validate minimum requirements
steps = workflow.get("steps", [])
if len(steps) < 3:
return {
"status": "NO_WORKFLOW",
"reason": f"Only {len(steps)} steps found, minimum is 3",
"repository": repo,
}
if workflow.get("confidence", 0) < 0.7:
return {
"status": "LOW_CONFIDENCE",
"confidence": workflow.get("confidence"),
"repository": repo,
}
return {
"status": "EXTRACTED",
"repository": repo,
"workflow": workflow,
}