Initial: Mesh-Skills, Configs, Scripts

This commit is contained in:
root
2026-09-02 23:49:07 +02:00
commit 26c7980750
783 changed files with 145393 additions and 0 deletions
@@ -0,0 +1,162 @@
#!/usr/bin/env python3
"""
daily_model_check.py
Ebene 1 des Skills: läuft 1x täglich (Cron), baut die grobe
Modell-Prioritätsliste pro Agent und schreibt sie nach
state/model_priority.json.
Nutzt aktuell Mock-Daten (mock_data.py). Für den Wechsel auf die
echte OpenRouter-API muss nur fetch_model_list() in mock_data.py
ersetzt werden - dieses Script bleibt unverändert.
Aufruf:
python3 daily_model_check.py
"""
import json
import sys
from pathlib import Path
import yaml
sys.path.insert(0, str(Path(__file__).parent))
from mock_data import fetch_model_list, fetch_timestamp # noqa: E402
CONFIG_PATH = Path(__file__).parent.parent / "config" / "agents.yaml"
STATE_DIR = Path(__file__).parent.parent / "state"
OUTPUT_PATH = STATE_DIR / "model_priority.json"
def load_config():
with open(CONFIG_PATH, "r", encoding="utf-8") as f:
return yaml.safe_load(f)
def index_models_by_id(models):
return {m["id"]: m for m in models}
def build_agent_priority(agent_name, agent_cfg, models_by_id, min_context, max_escalation_factor):
"""Baut die Eskalationsleiter für einen Agent, gefiltert nach
Mindest-Kontext und verfügbaren/bekannten Modellen.
"""
ladder = []
warnings = []
for step_idx, step in enumerate(agent_cfg["escalation_ladder"]):
model_id = step["model"]
cost_factor = step["cost_factor"]
# Free-Tier-Router-Strings wie "openrouter/free" stehen nicht in
# der Modell-Liste selbst - die werden unverändert übernommen.
if model_id not in models_by_id:
if model_id.startswith("openrouter/"):
ladder.append({"model": model_id, "cost_factor": cost_factor, "note": "auto-router, nicht einzeln geprüft"})
continue
warnings.append(f"{agent_name}: Modell '{model_id}' nicht in aktueller Modell-Liste gefunden - übersprungen")
continue
model = models_by_id[model_id]
if not model.get("available", True):
warnings.append(f"{agent_name}: Modell '{model_id}' aktuell nicht verfügbar - übersprungen")
continue
if model["context_length"] < min_context:
warnings.append(
f"{agent_name}: Modell '{model_id}' hat nur {model['context_length']} Kontext, "
f"unter Minimum von {min_context} - übersprungen"
)
continue
# Verhältnismäßigkeits-Check: Stufen jenseits von Stufe 0 dürfen
# den max_auto_escalation_factor nicht überschreiten, sonst braucht's
# manual_override.
if step_idx > 0 and cost_factor > max_escalation_factor and not agent_cfg.get("manual_override_allowed", False):
warnings.append(
f"{agent_name}: Eskalationsstufe '{model_id}' hat Faktor {cost_factor}x, "
f"über dem erlaubten Automatik-Limit von {max_escalation_factor}x - "
f"nur per manual_override nutzbar, aus Automatik-Leiter entfernt"
)
continue
ladder.append({
"model": model_id,
"cost_factor": cost_factor,
"context_length": model["context_length"],
"price_per_1k_tokens_eur": model["price_per_1k_tokens_eur"],
"family": model["family"],
})
return ladder, warnings
def check_diversity(priority_by_agent, min_distinct_families):
families_in_use = set()
for agent_name, ladder in priority_by_agent.items():
for step in ladder:
if "family" in step:
families_in_use.add(step["family"])
diverse_enough = len(families_in_use) >= min_distinct_families
return diverse_enough, sorted(families_in_use)
def main():
config = load_config()
global_cfg = config["global"]
min_context = global_cfg["min_context_tokens"]
max_escalation_factor = global_cfg["max_auto_escalation_factor"]
min_distinct_families = global_cfg["min_distinct_model_families"]
models = fetch_model_list()
models_by_id = index_models_by_id(models)
priority_by_agent = {}
all_warnings = []
for agent_name, agent_cfg in config["agents"].items():
ladder, warnings = build_agent_priority(
agent_name, agent_cfg, models_by_id, min_context, max_escalation_factor
)
priority_by_agent[agent_name] = ladder
all_warnings.extend(warnings)
if not ladder:
all_warnings.append(
f"WARNUNG: Agent '{agent_name}' hat NACH Filterung keine nutzbaren Modelle mehr - "
f"lokaler Fallback sollte hier greifen"
)
diverse_enough, families_in_use = check_diversity(priority_by_agent, min_distinct_families)
if not diverse_enough:
all_warnings.append(
f"WARNUNG: Nur {len(families_in_use)} Modell-Familien im Einsatz "
f"({', '.join(families_in_use)}), Minimum wäre {min_distinct_families}"
)
output = {
"generated_at": fetch_timestamp(),
"min_context_tokens": min_context,
"max_auto_escalation_factor": max_escalation_factor,
"diversity_ok": diverse_enough,
"model_families_in_use": families_in_use,
"agents": priority_by_agent,
"local_fallback": config["local_fallback"],
"warnings": all_warnings,
}
STATE_DIR.mkdir(exist_ok=True)
with open(OUTPUT_PATH, "w", encoding="utf-8") as f:
json.dump(output, f, indent=2, ensure_ascii=False)
print(f"Prioritätsliste geschrieben nach {OUTPUT_PATH}")
if all_warnings:
print(f"\n{len(all_warnings)} Warnung(en):")
for w in all_warnings:
print(f" - {w}")
if __name__ == "__main__":
main()
@@ -0,0 +1,114 @@
#!/usr/bin/env python3
"""
hourly_finetune_check.py
Ebene 2 des Skills: läuft stündlich (oder öfter), prüft NUR Deltas
gegen die von daily_model_check.py gebaute Prioritätsliste:
- Ist ein Agent über sein Tagesbudget (hard_stop_eur_per_day)?
-> Kill-Switch: Agent wird auf lokalen Fallback gezwungen.
- Wurde ein Modell inzwischen als nicht mehr verfügbar gemeldet?
-> Wird aus der aktiven Leiter entfernt (bis zum nächsten Tages-Lauf).
Erwartet, dass der jeweilige Agent selbst fortlaufend seinen
Tagesverbrauch nach state/spend_today.json schreibt, Format:
{ "agent_a": 0.42, "agent_b": 1.10, ... }
Falls diese Datei fehlt (z.B. noch nicht angebunden), läuft der Check
trotzdem durch und markiert das als Hinweis statt Fehler.
Aufruf:
python3 hourly_finetune_check.py
"""
import json
from pathlib import Path
import yaml
CONFIG_PATH = Path(__file__).parent.parent / "config" / "agents.yaml"
STATE_DIR = Path(__file__).parent.parent / "state"
PRIORITY_PATH = STATE_DIR / "model_priority.json"
SPEND_PATH = STATE_DIR / "spend_today.json"
KILL_SWITCH_PATH = STATE_DIR / "kill_switches.json"
def load_config():
with open(CONFIG_PATH, "r", encoding="utf-8") as f:
return yaml.safe_load(f)
def load_priority():
if not PRIORITY_PATH.exists():
raise FileNotFoundError(
f"{PRIORITY_PATH} fehlt - bitte zuerst daily_model_check.py laufen lassen"
)
with open(PRIORITY_PATH, "r", encoding="utf-8") as f:
return json.load(f)
def load_spend():
if not SPEND_PATH.exists():
return {}, ["spend_today.json fehlt noch - Budget-Check übersprungen, bitte Anbindung nachholen"]
with open(SPEND_PATH, "r", encoding="utf-8") as f:
return json.load(f), []
def check_budgets(config, spend):
kill_switches = {}
notes = []
for agent_name, agent_cfg in config["agents"].items():
limit = agent_cfg.get("hard_stop_eur_per_day")
spent = spend.get(agent_name)
if spent is None:
continue
if limit is None or limit == "BUDGET_PLACEHOLDER":
notes.append(
f"{agent_name}: hard_stop_eur_per_day ist noch Platzhalter - "
f"kein Kill-Switch aktiv, bitte echten Wert eintragen"
)
continue
if spent >= float(limit):
kill_switches[agent_name] = {
"reason": "daily_budget_exceeded",
"spent_eur": spent,
"limit_eur": limit,
"action": "postpone_to_next_day",
"basic_questions_fallback": "vps_ollama",
}
notes.append(
f"KILL-SWITCH: {agent_name} hat {spent}€ von {limit}€ Tageslimit erreicht - "
f"restliche Tasks werden auf morgen verschoben. Grundfragen laufen bis dahin "
f"weiter über das lokale VPS-Ollama-Modell, sofern das inhaltlich passt."
)
return kill_switches, notes
def main():
config = load_config()
priority = load_priority()
spend, spend_notes = load_spend()
kill_switches, budget_notes = check_budgets(config, spend)
result = {
"checked_at": priority.get("generated_at"),
"kill_switches": kill_switches,
"notes": spend_notes + budget_notes,
}
with open(KILL_SWITCH_PATH, "w", encoding="utf-8") as f:
json.dump(result, f, indent=2, ensure_ascii=False)
print(f"Feintuning-Check abgeschlossen, Ergebnis in {KILL_SWITCH_PATH}")
for note in result["notes"]:
print(f" - {note}")
if __name__ == "__main__":
main()
@@ -0,0 +1,104 @@
"""
Mock-Datenquelle für die OpenRouter-Modell-Liste.
Diese Funktion hat exakt die Signatur, die auch der echte API-Call später
haben wird (siehe TODO), damit daily_model_check.py unverändert bleibt,
wenn die echte API eingehängt wird.
TODO (später, wenn echte API gewünscht):
import requests
def fetch_model_list():
resp = requests.get(
"https://openrouter.ai/api/v1/models",
headers={"Authorization": f"Bearer {OPENROUTER_API_KEY}"},
timeout=10,
)
resp.raise_for_status()
return resp.json()["data"]
"""
from datetime import datetime, timezone
def fetch_model_list():
"""Gibt eine Liste von Modell-Dicts zurück, im gleichen Format wie
die echte OpenRouter-API (vereinfacht auf die Felder, die wir brauchen).
"""
return [
{
"id": "nvidia/nemotron-3-super",
"context_length": 262000,
"price_per_1k_tokens_eur": 0.0009,
"family": "nemotron",
"tags": ["linux_system", "allround"],
"available": True,
},
{
"id": "nvidia/nemotron-3-ultra",
"context_length": 1000000,
"price_per_1k_tokens_eur": 0.0027,
"family": "nemotron",
"tags": ["linux_system", "allround", "orchestration"],
"available": True,
},
{
"id": "deepseek/deepseek-flash",
"context_length": 128000,
"price_per_1k_tokens_eur": 0.0006,
"family": "deepseek",
"tags": ["code", "logic", "database"],
"available": True,
},
{
"id": "deepseek/deepseek-pro",
"context_length": 128000,
"price_per_1k_tokens_eur": 0.0018,
"family": "deepseek",
"tags": ["code", "logic", "database"],
"available": True,
},
{
"id": "qwen/qwen3-coder:free",
"context_length": 1000000,
"price_per_1k_tokens_eur": 0.0,
"family": "qwen",
"tags": ["homebrew_code", "prompt_generation", "code"],
"available": True,
},
{
"id": "qwen/qwen2.5-7b",
"context_length": 128000,
"price_per_1k_tokens_eur": 0.0003,
"family": "qwen",
"tags": ["javascript", "database_query"],
"available": True,
},
{
"id": "google/gemini-flash",
"context_length": 1000000,
"price_per_1k_tokens_eur": 0.0004,
"family": "gemini",
"tags": ["document_generation", "html_css_js"],
"available": True,
},
{
"id": "anthropic/claude-sonnet",
"context_length": 200000,
"price_per_1k_tokens_eur": 0.0045,
"family": "anthropic",
"tags": ["document_generation", "html_css_js", "allround"],
"available": True,
},
{
"id": "anthropic/claude-opus",
"context_length": 200000,
"price_per_1k_tokens_eur": 0.0900,
"family": "anthropic",
"tags": ["document_generation", "html_css_js", "allround"],
"available": True,
},
]
def fetch_timestamp():
return datetime.now(timezone.utc).isoformat()
@@ -0,0 +1,132 @@
#!/usr/bin/env python3
"""
task_agent_matcher.py
Ebene 3 des Skills: läuft bei Agent A (Master) zur Laufzeit, sobald ein
neuer Task ansteht. Entscheidet, welcher Agent bei welchem Standard-Modell
am besten zum Task-Typ passt - basierend auf:
- der von daily_model_check.py gebauten Prioritätsliste
(state/model_priority.json)
- aktiven Kill-Switches (state/kill_switches.json), z.B. wegen
Budget-Überschreitung -> Agent wird übersprungen bzw. auf
"postpone_to_next_day" gesetzt
- den Capability-Tags pro Agent (config/agents.yaml)
Jeder Agent bekommt exakt dieses Konzept (Config + Prioritätsliste)
mitgeliefert, damit alle konsistent nach derselben Logik laufen, egal
wer die Entscheidung am Ende ausführt.
Aufruf (Beispiel):
python3 task_agent_matcher.py --task-tag javascript
python3 task_agent_matcher.py --task-tag homebrew_code
"""
import argparse
import json
from pathlib import Path
import yaml
CONFIG_PATH = Path(__file__).parent.parent / "config" / "agents.yaml"
STATE_DIR = Path(__file__).parent.parent / "state"
PRIORITY_PATH = STATE_DIR / "model_priority.json"
KILL_SWITCH_PATH = STATE_DIR / "kill_switches.json"
def load_yaml(path):
with open(path, "r", encoding="utf-8") as f:
return yaml.safe_load(f)
def load_json(path, default=None):
if not path.exists():
return default if default is not None else {}
with open(path, "r", encoding="utf-8") as f:
return json.load(f)
def find_matches(task_tag, config, priority, kill_switches):
"""Gibt eine nach Preis sortierte Liste von (agent, model, price) für
Agents zurück, deren Capability-Tags zum Task-Typ passen und die
NICHT gerade per Kill-Switch auf 'postpone_to_next_day' stehen.
"""
candidates = []
postponed = []
for agent_name, agent_cfg in config["agents"].items():
if task_tag not in agent_cfg.get("capability_tags", []):
continue
if agent_name in kill_switches.get("kill_switches", {}):
ks = kill_switches["kill_switches"][agent_name]
postponed.append({
"agent": agent_name,
"reason": ks["reason"],
"action": ks["action"],
"fallback": ks.get("basic_questions_fallback"),
})
continue
ladder = priority.get("agents", {}).get(agent_name, [])
for step in ladder:
candidates.append({
"agent": agent_name,
"model": step["model"],
"cost_factor": step.get("cost_factor"),
"price_per_1k_tokens_eur": step.get("price_per_1k_tokens_eur"),
})
# Günstigste zuerst; Steps ohne Preisangabe (z.B. Free-Router) ganz vorne
candidates.sort(key=lambda c: (c["price_per_1k_tokens_eur"] is not None, c.get("price_per_1k_tokens_eur") or 0))
return candidates, postponed
def main():
parser = argparse.ArgumentParser(description="Task-zu-Agent-Matching (Ebene 3)")
parser.add_argument("--task-tag", required=True, help="Capability-Tag des Tasks, z.B. javascript, homebrew_code")
args = parser.parse_args()
config = load_yaml(CONFIG_PATH)
priority = load_json(PRIORITY_PATH)
kill_switches = load_json(KILL_SWITCH_PATH)
if not priority:
print("FEHLER: state/model_priority.json fehlt - bitte zuerst daily_model_check.py laufen lassen")
return
candidates, postponed = find_matches(args.task_tag, config, priority, kill_switches)
if not candidates:
local_fb = config.get("local_fallback", {})
if args.task_tag in local_fb.get("allowed_for_task_tags", []):
print(f"Kein Cloud-Kandidat für '{args.task_tag}' verfügbar (evtl. alle betroffenen Agents "
f"per Kill-Switch verschoben) - lokaler Fallback erlaubt:")
print(f" -> {local_fb['model']} (kostenfrei, VPS-Ollama)")
else:
print(f"Kein Kandidat für '{args.task_tag}' verfügbar und lokaler Fallback für diesen "
f"Task-Typ NICHT erlaubt (siehe config: local_fallback.allowed_for_task_tags). "
f"Task sollte pausiert werden.")
for p in postponed:
print(f" (verschoben: {p['agent']} - {p['reason']}, Fallback: {p['fallback']})")
return
best = candidates[0]
print(f"Beste Wahl für Task-Tag '{args.task_tag}':")
print(f" Agent: {best['agent']}")
print(f" Modell: {best['model']} (Kostenfaktor {best['cost_factor']}x)")
if len(candidates) > 1:
print("\nWeitere passende Kandidaten (Eskalationsreserve):")
for c in candidates[1:]:
print(f" - {c['agent']} / {c['model']} (Faktor {c['cost_factor']}x)")
if postponed:
print("\nHinweis, folgende Agents sind aktuell wegen Budget verschoben:")
for p in postponed:
print(f" - {p['agent']}: {p['reason']} -> {p['action']} (Grundfragen: {p['fallback']})")
if __name__ == "__main__":
main()