dc40d4c0db
- 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
207 lines
7.5 KiB
Python
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,
|
|
}
|