feat(eval): LLM-as-judge evaluators for finding actionability and review self-consistency
Two llm_as_judge evaluators score the review generation directly: a NUMERIC 0-1 on finding actionability, a BOOLEAN on whether the summary agrees with the findings. Both run on every observation whose trace name is pr-review or opencode-review. The judge is kimi-k2.7-code through the headroom hub. Local Ollama returns Anthropic-format responses but the thinking blocks lack the signature field Langfuse Zod schema requires; the evaluator preflight fails as Invalid JSON response. A small judge-proxy pod on 8802 forwards to the hub and patches every thinking block with a synthetic signature before returning. Trace + generation output now includes the findings themselves (capped at 25) rather than just the count, so a judge has something to grade. generation input/output mirrors the trace so an observation-level evaluator can read them. Idempotent: existing evaluators and rules are skipped on re-run, not duplicated. The connection is upserted on provider.
This commit is contained in:
@@ -0,0 +1,82 @@
|
||||
# Judge-side think-block patcher. Stands between Langfuse evaluators and the
|
||||
# headroom-ollama hub (port 8790). Local Ollama does not emit the `signature`
|
||||
# field that Langfuse's Anthropic adapter's Zod schema requires on every
|
||||
# `thinking` content block — without it, the evaluator preflight fails as
|
||||
# "Invalid JSON response". The proxy forwards /v1/* verbatim and adds a dummy
|
||||
# signature to each thinking block before returning.
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: judge-proxy
|
||||
namespace: pragent
|
||||
data:
|
||||
proxy.py: |
|
||||
#!/usr/bin/env python3
|
||||
"""Judge proxy: forward to headroom-ollama, fix thinking blocks."""
|
||||
import json, sys, urllib.request, urllib.error
|
||||
from http.server import BaseHTTPRequestHandler, HTTPServer
|
||||
from socketserver import ThreadingMixIn
|
||||
UPSTREAM = "http://100.74.17.70:8790"
|
||||
DUMMY_SIG = "kimi-local-judge-no-signature"
|
||||
class H(BaseHTTPRequestHandler):
|
||||
def _proxy(self):
|
||||
n = int(self.headers.get("Content-Length", 0))
|
||||
body = self.rfile.read(n) if n else b""
|
||||
h = {k: v for k, v in self.headers.items() if k.lower() not in ("host", "content-length")}
|
||||
req = urllib.request.Request(UPSTREAM + self.path, data=body, headers=h, method=self.command)
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=120) as r:
|
||||
resp_body = r.read(); status = r.status; rh = dict(r.headers)
|
||||
except urllib.error.HTTPError as e:
|
||||
resp_body = e.read(); status = e.code; rh = dict(e.headers)
|
||||
ct = rh.get("content-type", "")
|
||||
if status == 200 and "application/json" in ct and self.path.startswith("/v1/messages"):
|
||||
try:
|
||||
obj = json.loads(resp_body)
|
||||
patched = 0
|
||||
for blk in obj.get("content") or []:
|
||||
if isinstance(blk, dict) and blk.get("type") == "thinking" and "signature" not in blk:
|
||||
blk["signature"] = DUMMY_SIG; patched += 1
|
||||
if patched:
|
||||
resp_body = json.dumps(obj).encode("utf-8")
|
||||
rh["content-length"] = str(len(resp_body))
|
||||
print(f"judge-proxy: patched {patched} thinking block(s)", file=sys.stderr, flush=True)
|
||||
except Exception as e:
|
||||
print(f"judge-proxy: patch failed: {e}", file=sys.stderr, flush=True)
|
||||
self.send_response(status)
|
||||
for k, v in rh.items():
|
||||
if k.lower() not in ("transfer-encoding", "content-length", "connection"):
|
||||
self.send_header(k, v)
|
||||
self.send_header("Content-Length", str(len(resp_body)))
|
||||
self.end_headers(); self.wfile.write(resp_body)
|
||||
def do_POST(self): self._proxy()
|
||||
def do_GET(self): self._proxy()
|
||||
def log_message(self, *a, **k): pass
|
||||
class S(ThreadingMixIn, HTTPServer): daemon_threads = True
|
||||
S(("0.0.0.0", 8802), H).serve_forever()
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Pod
|
||||
metadata:
|
||||
name: judge-proxy
|
||||
namespace: pragent
|
||||
labels:
|
||||
app: judge-proxy
|
||||
spec:
|
||||
nodeSelector:
|
||||
kubernetes.io/hostname: kubernets
|
||||
hostNetwork: true
|
||||
dnsPolicy: ClusterFirstWithHostNet
|
||||
restartPolicy: Always
|
||||
containers:
|
||||
- name: p
|
||||
image: python:3.12-alpine
|
||||
command: ["sh","-c","apk add --no-cache ca-certificates >/dev/null && python3 -u /etc/cfg/proxy.py"]
|
||||
volumeMounts:
|
||||
- {name: cfg, mountPath: /etc/cfg}
|
||||
ports:
|
||||
- {containerPort: 8802, hostPort: 8802}
|
||||
volumes:
|
||||
- name: cfg
|
||||
configMap:
|
||||
name: judge-proxy
|
||||
Reference in New Issue
Block a user