rgboards/rg-boards
Lanes, team task boards you run from the web or from Claude/ChatGPT (MCP). FastAPI + DataK3 + appid + Dodil Models.
A remix template · 0 remixes · Remix rgboards/rg-boards
app/boards/infrastructure/dodil.py
"""The Dodil side: a service-account token, DataK3 on the Postgres wire, and Dodil Models.
The remix generated the service account and put its id and secret into the repo's CI secrets;
.dodil/deploy.yaml hands them to the app as environment variables.
"""
import os
import threading
import time
import httpx
import psycopg
TOKEN_URL = "https://id.dodil.io/realms/dodil/protocol/openid-connect/token"
PG_HOST = os.environ.get("PG_HOST", "pg.uk-lon-1.dodil.io")
MODELS_BASE = os.environ.get("MODELS_BASE", "https://api.dodil.io/v1").rstrip("/")
CHAT_MODEL = os.environ.get("CHAT_MODEL", "kimi-k2.6")
EMBED_MODEL = os.environ.get("EMBED_MODEL", "jina-embeddings-v4")
_lock, _tok = threading.Lock(), {"v": None, "exp": 0.0}
def token() -> str:
"""A short-lived service-account token (client_credentials), cached until near expiry.
It is the DataK3 password AND the Models bearer.
Local only: DODIL_TOKEN=<your own `dodil auth` token> skips the service account."""
if os.environ.get("DODIL_TOKEN"):
return os.environ["DODIL_TOKEN"]
with _lock:
if _tok["v"] and time.time() < _tok["exp"] - 60:
return _tok["v"]
r = httpx.post(TOKEN_URL, timeout=15, data={
"grant_type": "client_credentials",
"client_id": os.environ["DODIL_SERVICE_ACCOUNT_ID"],
"client_secret": os.environ["DODIL_SERVICE_ACCOUNT_SECRET"]})
r.raise_for_status()
d = r.json()
_tok.update(v=d["access_token"], exp=time.time() + float(d.get("expires_in", 300)))
return _tok["v"]
def connect() -> psycopg.Connection:
"""DataK3 speaks Postgres: the database is the bucket, TLS is required."""
return psycopg.connect(host=PG_HOST, port=5432, dbname=os.environ["BUCKET"], user="token",
password=token(), sslmode="require", autocommit=True)
def _unwrap(body: dict) -> dict:
"""The gateway may wrap the OpenAI body as {"status": ..., "data": {...}}."""
inner = body.get("data") if isinstance(body, dict) else None
if isinstance(inner, dict) and ("choices" in inner or "data" in inner):
return inner
return body
def chat(prompt: str, system: str = "") -> str:
"""One chat call to Dodil Models (OpenAI-compatible)."""
messages = ([{"role": "system", "content": system}] if system else []) + \
[{"role": "user", "content": prompt}]
r = httpx.post(f"{MODELS_BASE}/chat/completions", timeout=120,
headers={"Authorization": f"Bearer {token()}"},
json={"model": CHAT_MODEL, "messages": messages})
r.raise_for_status()
body = _unwrap(r.json())
return (body["choices"][0]["message"].get("content") or "").strip()
def embed(texts: list) -> list:
"""Embeddings from Dodil Models (jina-embeddings-v4), in input order."""
out = []
for i in range(0, len(texts), 32):
r = httpx.post(f"{MODELS_BASE}/embeddings", timeout=120,
headers={"Authorization": f"Bearer {token()}"},
json={"model": EMBED_MODEL, "input": [t[:4000] for t in texts[i:i + 32]]})
r.raise_for_status()
rows = sorted(_unwrap(r.json())["data"], key=lambda d: d.get("index", 0))
out += [d["embedding"] for d in rows]
return out
class ModelsAssistant:
def checklist(self, card_title: str, description: str = "") -> list:
text = chat("Break this task into a short checklist of 3 to 6 concrete steps. "
"One step per line, no numbering, no preamble, nothing else.\n\n"
f"Task: {card_title}\n{description}".strip())
steps = [line.strip().lstrip("-*•0123456789.)[] ").strip() for line in text.splitlines()]
return [s for s in steps if s][:8]
def summary(self, board_title: str, facts: str) -> str:
return chat(f"Board: {board_title}\n{facts}",
system="You write a short daily summary of a team task board for its "
"members: at most 5 bullet points covering what moved, what is "
"overdue, what is blocked and who should look at what. Plain text, "
"use the card numbers like #12. No preamble.")
def embed(self, texts: list) -> list:
return embed(texts)