diy-creator / llm.py
BonusLockSMith's picture
#17 DIY Creator — multi-agent research/write/critique team
a5c7816 verified
Raw History Blame Contribute Delete
4.73 kB
#!/usr/bin/env python
"""LLM backend for the DIY Creator multi-agent team (#17): local Ollama (default) or Claude.
Two entry points:
chat(system, user) -> free text (the writer / reviser produce markdown guides)
json_call(system, user) -> parsed JSON (the researcher's brief, the critic's verdict)
Each call takes an optional `model` so different ROLES can run on different models — which is the
whole point of the critic: it can be a *different* model from the writer, so the review is
independent (the "diamond"), not the author grading its own work. Errors log the exception TYPE only.
"""
import json
import os
import urllib.request
BACKEND = os.environ.get("DIY_LLM_BACKEND", "ollama").lower()
OLLAMA_URL = os.environ.get("DIY_OLLAMA_URL", "http://127.0.0.1:11434").rstrip("/")
OLLAMA_MODEL = os.environ.get("DIY_OLLAMA_MODEL", "qwen2.5:7b")
# The critic defaults to the same model, but set DIY_CRITIC_MODEL to a *different* one for a truly
# independent review (e.g. a different family, or a bigger model where the quality bar matters).
CRITIC_MODEL = os.environ.get("DIY_CRITIC_MODEL", "").strip() or OLLAMA_MODEL
ANTHROPIC_KEY = (os.environ.get("ANTHROPIC_API_KEY") or "").strip() or None
ANTHROPIC_MODEL = os.environ.get("ANTHROPIC_MODEL", "claude-haiku-4-5")
ANTHROPIC_CRITIC = os.environ.get("ANTHROPIC_CRITIC_MODEL", "").strip() or ANTHROPIC_MODEL
_USE_CLAUDE = BACKEND == "claude" and ANTHROPIC_KEY
def _ollama(system, user, model, temperature, max_tokens, as_json):
payload = {"model": model, "stream": False,
"messages": [{"role": "system", "content": system}, {"role": "user", "content": user}],
"options": {"temperature": temperature, "num_predict": max_tokens}}
if as_json:
payload["format"] = "json" # Ollama constrains output to valid JSON
body = json.dumps(payload).encode()
req = urllib.request.Request(OLLAMA_URL + "/api/chat", data=body,
headers={"Content-Type": "application/json"})
return json.loads(urllib.request.urlopen(req, timeout=180).read())["message"]["content"].strip()
def _claude(system, user, model, temperature, max_tokens, as_json):
if as_json:
system = system + "\nReply with ONLY valid JSON — no prose, no code fences."
body = json.dumps({"model": model, "max_tokens": max_tokens, "temperature": temperature,
"system": system, "messages": [{"role": "user", "content": user}]}).encode()
req = urllib.request.Request("https://api.anthropic.com/v1/messages", data=body, headers={
"x-api-key": ANTHROPIC_KEY, "anthropic-version": "2023-06-01", "content-type": "application/json"})
data = json.loads(urllib.request.urlopen(req, timeout=180).read())
return "".join(b.get("text", "") for b in data.get("content", [])).strip()
def _resolve(model, role):
"""Pick the concrete model id for a role given the active backend."""
if _USE_CLAUDE:
return ANTHROPIC_CRITIC if role == "critic" else ANTHROPIC_MODEL
if role == "critic":
return CRITIC_MODEL
return model or OLLAMA_MODEL
def chat(system: str, user: str, role: str = "", model: str = None,
temperature: float = 0.5, max_tokens: int = 1400) -> str:
m = _resolve(model, role)
fn = _claude if _USE_CLAUDE else _ollama
return fn(system, user, m, temperature, max_tokens, as_json=False)
def json_call(system: str, user: str, role: str = "", model: str = None,
temperature: float = 0.2, max_tokens: int = 1200):
m = _resolve(model, role)
fn = _claude if _USE_CLAUDE else _ollama
raw = fn(system, user, m, temperature, max_tokens, as_json=True)
return _parse_json(raw)
def _parse_json(raw: str):
s = (raw or "").strip()
if s.startswith("```"):
s = s.split("```", 2)[1].lstrip("json").strip() if s.count("```") >= 2 else s.strip("`")
try:
return json.loads(s)
except Exception:
pass
# salvage the first {...} or [...] block
for open_c, close_c in (("{", "}"), ("[", "]")):
i, j = s.find(open_c), s.rfind(close_c)
if 0 <= i < j:
try:
return json.loads(s[i:j + 1])
except Exception:
continue
return None
def backend_label() -> str:
if _USE_CLAUDE:
extra = f" · critic: {ANTHROPIC_CRITIC}" if ANTHROPIC_CRITIC != ANTHROPIC_MODEL else ""
return f"Claude ({ANTHROPIC_MODEL}){extra}"
extra = f" · critic: {CRITIC_MODEL}" if CRITIC_MODEL != OLLAMA_MODEL else ""
return f"Ollama ({OLLAMA_MODEL} @ {OLLAMA_URL}){extra}"