#!/usr/bin/env python3 """ Prompt Pipeline — 1660 generates prompts, bigger GPUs process them through Mortdecai. Architecture: 1660 Super (qwen3.5:0.8b) → generates diverse edge-case prompts → writes to shared queue file 2080 Ti / RTX 4000 / 3090 Ti (mortdecai) → reads queue, processes, validates via RCON Usage: # Terminal 1: prompt generator (runs on 1660) python3 prompt_pipeline.py --mode generate --ollama-url http://192.168.0.235:11434 # Terminal 2+: processors (one per GPU) python3 prompt_pipeline.py --mode process --ollama-url http://192.168.0.141:11434 python3 prompt_pipeline.py --mode process --ollama-url http://192.168.0.179:11434 # Or run everything: python3 prompt_pipeline.py --mode all """ import argparse import json import os import re import random import sys import time import threading import fcntl from pathlib import Path import requests ROOT = Path(__file__).resolve().parent.parent.parent sys.path.insert(0, str(ROOT)) from agent.tools.persistent_rcon import get_rcon QUEUE_FILE = ROOT / "data" / "processed" / "prompt_queue.jsonl" OUTPUT_FILE = ROOT / "data" / "processed" / "pipeline_output.jsonl" LOCK_FILE = str(QUEUE_FILE) + ".lock" # Categories the model needs more training on (based on validator hit rates and bake-off failures) GENERATION_PROMPTS = { "fill_syntax": """Generate 5 Minecraft player chat messages that involve filling areas with blocks. Include: walls, floors, replacing blocks, clearing areas, building structures. The AI often gets fill syntax wrong — adding a trailing count number or wrong coordinates. Every message must start with "sudo ". Return ONLY a JSON array of strings.""", "enchantments": """Generate 5 Minecraft player chat messages requesting enchanted items. Include: multiple enchantments, max level enchants, mutually exclusive combinations, items that cant be enchanted. The AI sometimes uses old NBT syntax instead of 1.21 component syntax. Every message must start with "sudo " or "pray ". Return ONLY a JSON array of strings.""", "execute_chains": """Generate 5 Minecraft player chat messages requiring execute command chains. Include: execute as/at/if/positioned/in, targeting by gamemode/team, dimension switching, conditional blocks. Every message must start with "sudo ". Return ONLY a JSON array of strings.""", "entity_targeting": """Generate 5 Minecraft player chat messages about killing or targeting specific entities. Include: "kill THE zombie" vs "kill ALL zombies", distance-based targeting, entity types, nearby vs far. Every message must start with "sudo ". Return ONLY a JSON array of strings.""", "gamerules_timed": """Generate 5 Minecraft player chat messages requesting temporary gamerule changes. Include: specific durations like "for 5 minutes", "for an hour", "briefly", "permanently". The AI needs to learn when to set revert timers vs permanent changes. Every message must start with "sudo ". Return ONLY a JSON array of strings.""", "memory_commands": """Generate 5 Minecraft player chat messages about remembering or recalling locations and facts. Include: "remember this as home", "tp me home", "what do you know about me", "forget my base". Every message must start with "sudo " or "pray ". Return ONLY a JSON array of strings.""", "creative_prayers": """Generate 5 creative Minecraft prayers to an AI God character. Include: humble requests, greedy demands, blasphemy, roleplay, emotional appeals, sarcasm. Be creative and diverse. Every message must start with "pray ". Return ONLY a JSON array of strings.""", "edge_items": """Generate 5 Minecraft player requests for obscure or easily-confused items. Include: items with color variants (wool, concrete, glass), items that changed names between versions, items people misspell (steak vs cooked_beef, log vs oak_log, bed vs white_bed). Every message must start with "sudo ". Return ONLY a JSON array of strings.""", "multicommand": """Generate 5 Minecraft player requests that need multiple commands to fulfill. Include: "gear me up for the nether", "build a house and tp me inside", "create a pvp arena", "prepare me for a boss fight". Complex multi-step requests. Every message must start with "sudo " or "pray ". Return ONLY a JSON array of strings.""", "natural_language": """Generate 5 Minecraft chat messages phrased in unusual natural language. Include: typos, slang, abbreviations, run-on sentences, vague requests, sarcasm, mixed languages. The AI should still figure out what the player wants. Every message must start with "sudo " or "pray ". Return ONLY a JSON array of strings.""", } def ensure_prefix(msg): lower = msg.lower().strip() if lower.startswith("pray ") or lower.startswith("sudo ") or lower.startswith("bug_log"): return msg return ("pray " if random.random() < 0.4 else "sudo ") + msg def generate_prompts(ollama_url, model="qwen3.5:0.8b", count=50): """Use a small model to generate diverse prompts.""" prompts = [] categories = list(GENERATION_PROMPTS.keys()) random.shuffle(categories) for cat in categories: if len(prompts) >= count: break try: r = requests.post(f"{ollama_url}/api/chat", json={ "model": model, "messages": [ {"role": "system", "content": "You generate Minecraft chat messages for AI training. Return ONLY a JSON array of strings."}, {"role": "user", "content": GENERATION_PROMPTS[cat]}, ], "stream": False, "options": {"temperature": 1.1, "num_predict": 400}, }, timeout=60) content = r.json()["message"]["content"] content = re.sub(r'[\s\S]*?\s*', '', content) cleaned = content.replace("```json", "").replace("```", "").strip() match = re.search(r'\[[\s\S]*\]', cleaned) if match: items = json.loads(match.group()) for item in items: if isinstance(item, str) and item.strip(): prompts.append({ "prompt": ensure_prefix(item.strip()), "category": cat, "generated_at": time.strftime("%Y-%m-%dT%H:%M:%SZ"), }) print(f" [{cat}] +{len(items) if match else 0} prompts") except Exception as e: print(f" [{cat}] ERROR: {e}") time.sleep(0.5) return prompts def write_queue(prompts): """Append prompts to the queue file with file locking.""" os.makedirs(os.path.dirname(QUEUE_FILE), exist_ok=True) with open(LOCK_FILE, "w") as lf: fcntl.flock(lf, fcntl.LOCK_EX) with open(QUEUE_FILE, "a") as f: for p in prompts: f.write(json.dumps(p) + "\n") fcntl.flock(lf, fcntl.LOCK_UN) print(f" Queued {len(prompts)} prompts") def read_queue(batch_size=5): """Read and remove prompts from the queue.""" if not QUEUE_FILE.exists(): return [] with open(LOCK_FILE, "w") as lf: fcntl.flock(lf, fcntl.LOCK_EX) with open(QUEUE_FILE) as f: lines = f.readlines() taken = lines[:batch_size] remaining = lines[batch_size:] with open(QUEUE_FILE, "w") as f: f.writelines(remaining) fcntl.flock(lf, fcntl.LOCK_UN) return [json.loads(l) for l in taken if l.strip()] def process_prompt(prompt_data, ollama_url, model, rcon_host, rcon_port, rcon_pass): """Process a single prompt through Mortdecai and validate via RCON.""" prompt = prompt_data["prompt"] category = prompt_data.get("category", "unknown") mode = "god" if prompt.lower().startswith("pray ") else "sudo" system = ('You are God in a Minecraft server. Return JSON: {"message": "...", "commands": ["cmd1"], "reasoning": "why"}' if mode == "god" else 'You are a Minecraft 1.21 command translator. Return JSON: {"commands": ["cmd1"], "reasoning": "why"}\n' 'IMPORTANT: Each command is ONE COMPLETE STRING. Use minecraft: prefix. Enchantments: item[enchantments={name:level}].') try: r = requests.post(f"{ollama_url}/api/chat", json={ "model": model, "messages": [ {"role": "system", "content": "/no_think\n" + system}, {"role": "user", "content": f"Player slingshooter08: {prompt}"}, ], "stream": False, "format": "json", "options": {"temperature": 0.3, "num_predict": 500}, }, timeout=120) content = r.json()["message"]["content"] content = re.sub(r'[\s\S]*?\s*', '', content) parsed = json.loads(content) except Exception as e: return {"prompt": prompt, "category": category, "error": str(e), "success": False} commands = parsed.get("commands") or [] if not commands or not all(isinstance(c, str) and " " in c for c in commands): return {"prompt": prompt, "category": category, "commands": commands, "success": False, "reason": "empty_or_split"} # Validate via RCON rcon = get_rcon(rcon_host, rcon_port, rcon_pass) results = [] all_ok = True for cmd in commands[:6]: try: result = rcon.command(cmd) is_error = "<--[HERE]" in result or "Unknown" in result or "Incorrect" in result results.append({"cmd": cmd, "result": result[:150], "ok": not is_error}) if is_error: all_ok = False except: results.append({"cmd": cmd, "result": "RCON error", "ok": False}) all_ok = False return { "prompt": prompt, "category": category, "mode": mode, "commands": commands, "rcon_results": results, "success": all_ok, "message": parsed.get("message", ""), "reasoning": parsed.get("reasoning", ""), "processed_at": time.strftime("%Y-%m-%dT%H:%M:%SZ"), "ollama_url": ollama_url, } def save_result(result): """Save a processed result.""" os.makedirs(os.path.dirname(OUTPUT_FILE), exist_ok=True) with open(OUTPUT_FILE, "a") as f: f.write(json.dumps(result, ensure_ascii=False) + "\n") def run_generator(ollama_url, model, interval=120): """Continuously generate prompts.""" print(f"Prompt Generator started") print(f" Model: {model} on {ollama_url}") print(f" Interval: {interval}s between batches") while True: print(f"\nGenerating batch at {time.strftime('%H:%M:%S')}...") prompts = generate_prompts(ollama_url, model, count=30) if prompts: write_queue(prompts) print(f" Queue size: {sum(1 for _ in open(QUEUE_FILE)) if QUEUE_FILE.exists() else 0}") time.sleep(interval) def run_processor(ollama_url, model, rcon_host, rcon_port, rcon_pass): """Continuously process prompts from the queue.""" print(f"Processor started") print(f" Model: {model} on {ollama_url}") print(f" RCON: {rcon_host}:{rcon_port}") stats = {"processed": 0, "success": 0, "failed": 0} while True: batch = read_queue(3) if not batch: time.sleep(5) continue for prompt_data in batch: result = process_prompt(prompt_data, ollama_url, model, rcon_host, rcon_port, rcon_pass) save_result(result) stats["processed"] += 1 if result.get("success"): stats["success"] += 1 print(f" OK [{result['category'][:12]}] {result['prompt'][:50]}") else: stats["failed"] += 1 print(f" FAIL [{result['category'][:12]}] {result['prompt'][:50]}") if stats["processed"] % 20 == 0: rate = stats["success"] / max(stats["processed"], 1) * 100 print(f" Stats: {stats['processed']} processed, {rate:.0f}% success") time.sleep(0.2) def main(): parser = argparse.ArgumentParser(description="Prompt generation pipeline") parser.add_argument("--mode", choices=["generate", "process", "all"], required=True) parser.add_argument("--gen-url", default="http://192.168.0.235:11434", help="Generator Ollama URL (small model)") parser.add_argument("--gen-model", default="qwen3.5:0.8b", help="Generator model") parser.add_argument("--proc-urls", default="http://192.168.0.141:11434,http://192.168.0.179:11434", help="Processor Ollama URLs (comma-separated)") parser.add_argument("--proc-model", default="mortdecai:0.4.0", help="Processor model") parser.add_argument("--rcon-host", default="192.168.0.244") parser.add_argument("--rcon-port", type=int, default=25578) parser.add_argument("--rcon-pass", default="REDACTED_RCON") parser.add_argument("--interval", type=int, default=120, help="Generator batch interval (seconds)") args = parser.parse_args() if args.mode == "generate": run_generator(args.gen_url, args.gen_model, args.interval) elif args.mode == "process": urls = args.proc_urls.split(",") run_processor(urls[0], args.proc_model, args.rcon_host, args.rcon_port, args.rcon_pass) elif args.mode == "all": urls = args.proc_urls.split(",") # Start generator in background gen_thread = threading.Thread(target=run_generator, args=(args.gen_url, args.gen_model, args.interval), daemon=True) gen_thread.start() # Start a processor per URL for url in urls: proc_thread = threading.Thread(target=run_processor, args=(url, args.proc_model, args.rcon_host, args.rcon_port, args.rcon_pass), daemon=True) proc_thread.start() print(f"Processor started on {url}") # Keep main alive while True: time.sleep(60) queue_size = sum(1 for _ in open(QUEUE_FILE)) if QUEUE_FILE.exists() else 0 output_size = sum(1 for _ in open(OUTPUT_FILE)) if OUTPUT_FILE.exists() else 0 print(f"[Pipeline] Queue: {queue_size} | Output: {output_size}") if __name__ == "__main__": main()