Files
Epictetus dc40d4c0db Pipeline v2: deterministic reviewer, implementation extraction, publisher fix
- Reader: discover workflow files in nested dirs (agents/, workflows/, examples/)
- Reader: load source code, config, deps — not just docs
- Extractor: prompt demands concrete implementation details (files, deps, code)
- Scorer: removed general_purpose check (5/6 checks, score 1.0)
- Generator: includes Setup, Key Files, Implementation Details sections
- Reviewer: replaced LLM review with 8 deterministic structural checks
- Publisher: handle 409 duplicate PR gracefully as success
- 5 skills published as PRs #6-#10 on Gitea
2026-08-05 13:58:38 +00:00

207 lines
7.5 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/review."""
llm_config = config.get("llm_pipeline", 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)
# Detect Ollama native API (11434 port) — use /api/chat instead of /v1/chat/completions
is_ollama_native = ":11434" in base_url
if is_ollama_native:
payload = {
"model": model,
"messages": [{"role": "user", "content": prompt}],
"stream": False,
"options": {"num_predict": max_tokens, "temperature": 0.1},
}
try:
resp = requests.post(f"{base_url}/api/chat", json=payload, headers={"Content-Type": "application/json"}, timeout=120)
if resp.status_code == 200:
return resp.json().get("message", {}).get("content", "")
else:
return f"LLM error: {resp.status_code}"
except Exception as e:
return f"LLM error: {str(e)}"
else:
# OpenAI-compatible format
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()
msg = data["choices"][0]["message"]
content = (msg.get("content") or msg.get("reasoning_content") or "").strip()
if not content and msg.get("reasoning_content"):
rc = msg["reasoning_content"]
import re
json_match = re.search(r'(\{.*\})', rc, re.DOTALL)
if json_match:
content = json_match.group()
return 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. Analyze a GitHub repository and determine if it contains a reusable AI workflow or pattern that another agent could learn from and actually implement.
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 with type description", "Input 2 with type description"],
"steps": [
"Step 1: Describe the specific action, mentioning the exact tool/function/file used (e.g. 'Run langgraph chain with agent.py')",
"Step 2: ...",
"Step 3: ..."
],
"outputs": ["Output 1 with description", "Output 2 with description"],
"failure_modes": ["Specific failure scenario with mitigation"],
"confidence": 0.95,
"reusable": true,
"general_purpose": false,
"explanation": "Why this is reusable",
"implementation_details": {{
"framework": "e.g. langchain, langgraph, autogen, crewai, custom",
"dependencies": ["python-packages-needed"],
"key_files": ["path/to/key_file.py - description"],
"code_snippets": ["Brief but concrete code or config example from the repo"],
"setup_steps": ["Prerequisite setup commands or configs"]
}}
}}
If the repository does NOT contain a reusable workflow, return:
{{
"has_workflow": false,
"reason": "Why no reusable workflow was found"
}}
CRITICAL: Steps must be SPECIFIC — mention actual file names, function calls, tool names, or configuration details from the repository. A step like 'Researcher agent gathers facts' is too vague. Instead: 'Researcher agent (agent.py) uses LangGraph create_react_agent with SerperDevTool to gather facts.'
Criteria for a reusable workflow:
- It describes a concrete process, not just a tool or library
- Steps mention specific implementations from the code
- It has clear inputs, steps, and outputs
- It could be adapted to different contexts
- It has at least 3 distinct steps
- It solves a real problem
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,
}