Skip to content

Commit 68fa0e9

Browse files
add LLM-as-a-Judge detections via vllm-judge
1 parent e41a429 commit 68fa0e9

File tree

5 files changed

+263
-0
lines changed

5 files changed

+263
-0
lines changed

detectors/llm_judge/README.md

Whitespace-only changes.

detectors/llm_judge/app.py

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import os
2+
import sys
3+
from contextlib import asynccontextmanager
4+
from typing import Annotated, Dict
5+
6+
from fastapi import Header
7+
from prometheus_fastapi_instrumentator import Instrumentator
8+
sys.path.insert(0, os.path.abspath(".."))
9+
10+
from common.app import DetectorBaseAPI as FastAPI
11+
from detector import LLMJudgeDetector
12+
from scheme import (
13+
ContentAnalysisHttpRequest,
14+
ContentsAnalysisResponse,
15+
MetricsListResponse,
16+
Error,
17+
)
18+
19+
detector_objects: Dict[str, LLMJudgeDetector] = {}
20+
21+
22+
@asynccontextmanager
23+
async def lifespan(app: FastAPI):
24+
"""Application lifespan management."""
25+
try:
26+
detector_objects["detector"] = LLMJudgeDetector()
27+
yield
28+
finally:
29+
# Clean up resources
30+
if "detector" in detector_objects:
31+
await detector_objects["detector"].close()
32+
detector_objects.clear()
33+
34+
35+
app = FastAPI(lifespan=lifespan, dependencies=[])
36+
Instrumentator().instrument(app).expose(app)
37+
38+
39+
@app.post(
40+
"/api/v1/text/contents",
41+
response_model=ContentsAnalysisResponse,
42+
description="""LLM-as-Judge detector that evaluates content using various metrics like safety, toxicity, accuracy, helpfulness, etc. \
43+
The metric parameter allows you to specify which evaluation criteria to use. \
44+
Supports all built-in vllm_judge metrics including safety, accuracy, helpfulness, clarity, and many more.""",
45+
responses={
46+
404: {"model": Error, "description": "Resource Not Found"},
47+
422: {"model": Error, "description": "Validation Error"},
48+
},
49+
)
50+
async def detector_unary_handler(
51+
request: ContentAnalysisHttpRequest,
52+
detector_id: Annotated[str, Header(example="llm_judge_safety")],
53+
):
54+
"""Analyze content using LLM-as-Judge evaluation."""
55+
return ContentsAnalysisResponse(root=await detector_objects["detector"].run(request))
56+
57+
58+
@app.get(
59+
"/api/v1/metrics",
60+
response_model=MetricsListResponse,
61+
description="List all available metrics for LLM Judge evaluation",
62+
responses={
63+
404: {"model": Error, "description": "Resource Not Found"},
64+
},
65+
)
66+
async def list_metrics():
67+
"""List all available evaluation metrics."""
68+
detector = detector_objects.get("detector")
69+
if not detector:
70+
return {"metrics": [], "total": 0}
71+
72+
metrics = detector.list_available_metrics()
73+
return MetricsListResponse(metrics=metrics, total=len(metrics))

detectors/llm_judge/detector.py

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
import os
2+
import sys
3+
from typing import List, Dict, Any
4+
5+
sys.path.insert(0, os.path.abspath(".."))
6+
7+
from vllm_judge import Judge, EvaluationResult, BUILTIN_METRICS
8+
from vllm_judge.exceptions import MetricNotFoundError
9+
from common.app import logger
10+
from scheme import (
11+
ContentAnalysisHttpRequest,
12+
ContentAnalysisResponse,
13+
ContentsAnalysisResponse,
14+
)
15+
16+
17+
class LLMJudgeDetector:
18+
"""LLM-as-Judge detector for evaluating content using vllm_judge."""
19+
20+
def __init__(self) -> None:
21+
"""Initialize the LLM Judge Detector."""
22+
self.judge = None
23+
self.available_metrics = set(BUILTIN_METRICS.keys())
24+
25+
# Get configuration from environment
26+
self.vllm_base_url = os.environ.get("VLLM_BASE_URL")
27+
self.vllm_model = os.environ.get("VLLM_MODEL")
28+
29+
if not self.vllm_base_url:
30+
raise ValueError("VLLM_BASE_URL environment variable is required")
31+
32+
logger.info(f"Initializing LLM Judge with URL: {self.vllm_base_url}")
33+
34+
self._initialize_judge()
35+
36+
def _initialize_judge(self) -> None:
37+
"""Initialize the vLLM Judge."""
38+
try:
39+
self.judge = Judge.from_url(base_url=self.vllm_base_url)
40+
logger.info(f"LLM Judge initialized successfully with model: {self.judge.config.model} and base url: {self.judge.config.base_url}")
41+
logger.info(f"Available metrics: {', '.join(sorted(self.available_metrics))}")
42+
43+
except Exception as e:
44+
logger.error(f"Failed to initialize LLM Judge: {e}")
45+
raise
46+
47+
async def evaluate_single_content(self, content: str, params: Dict[str, Any]) -> ContentAnalysisResponse:
48+
"""
49+
Evaluate a single piece of content using the specified metric.
50+
51+
Args:
52+
content: Text content to evaluate
53+
params: vLLM Judge parameters for the evaluation
54+
55+
Returns:
56+
ContentAnalysisResponse with evaluation results
57+
"""
58+
if "metric" not in params and "criteria" not in params:
59+
params["metric"] = "safety" # Default to safety
60+
61+
evaluation_params = {
62+
"content": content,
63+
**params
64+
}
65+
66+
# Perform evaluation
67+
result: EvaluationResult = await self.judge.evaluate(
68+
**evaluation_params
69+
)
70+
71+
# Convert to response format
72+
score = None
73+
if isinstance(result.decision, (int, float)) or result.score is not None:
74+
# Numeric result
75+
score = float(result.score if result.score is not None else result.decision)
76+
77+
return ContentAnalysisResponse(
78+
start=0,
79+
end=len(content),
80+
detection=str(result.decision),
81+
detection_type="llm_judge",
82+
score=score,
83+
text=content,
84+
evidences=[],
85+
metadata={"reasoning": result.reasoning}
86+
)
87+
88+
async def run(self, request: ContentAnalysisHttpRequest) -> ContentsAnalysisResponse:
89+
"""
90+
Run content analysis for each input text.
91+
92+
Args:
93+
request: Input request containing texts and metric to analyze
94+
95+
Returns:
96+
ContentsAnalysisResponse: The aggregated response for all input texts
97+
"""
98+
99+
contents_analyses = []
100+
101+
for content in request.contents:
102+
analysis = await self.evaluate_single_content(content, request.detector_params)
103+
contents_analyses.append([analysis]) # Wrap in list to match schema
104+
105+
return contents_analyses
106+
107+
108+
async def close(self):
109+
"""Close the judge client."""
110+
if self.judge:
111+
await self.judge.close()
112+
113+
def list_available_metrics(self) -> List[str]:
114+
"""Return list of available metrics."""
115+
return sorted(list(self.available_metrics))

detectors/llm_judge/requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
vllm-judge>=0.1.5

detectors/llm_judge/scheme.py

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
from enum import Enum
2+
from typing import List, Optional, Dict, Any
3+
from pydantic import BaseModel, Field, RootModel
4+
5+
6+
class Evidence(BaseModel):
7+
source: str = Field(
8+
title="Source",
9+
example="https://en.wikipedia.org/wiki/IBM",
10+
description="Source of the evidence, it can be url of the evidence etc",
11+
)
12+
13+
14+
class EvidenceType(str, Enum):
15+
url = "url"
16+
title = "title"
17+
18+
19+
class EvidenceObj(BaseModel):
20+
type: EvidenceType = Field(
21+
title="EvidenceType",
22+
example="url",
23+
description="Type field signifying the type of evidence provided. Example url, title etc",
24+
)
25+
evidence: Evidence = Field(
26+
description="Evidence object, currently only containing source, but in future can contain other optional arguments like id, etc",
27+
)
28+
29+
30+
class ContentAnalysisHttpRequest(BaseModel):
31+
contents: List[str] = Field(
32+
min_length=1,
33+
title="Contents",
34+
description="Field allowing users to provide list of texts for analysis. Note, results of this endpoint will contain analysis / detection of each of the provided text in the order they are present in the contents object.",
35+
example=[
36+
"Martians are like crocodiles; the more you give them meat, the more they want"
37+
],
38+
)
39+
detector_params: Optional[Dict[str, Any]] = Field(
40+
default_factory=dict,
41+
description="Detector parameters for evaluation (e.g., metric, criteria, etc.)",
42+
example={"metric": "safety"}
43+
)
44+
45+
46+
class ContentAnalysisResponse(BaseModel):
47+
start: int = Field(example=0)
48+
end: int = Field(example=75)
49+
text: str = Field(example="This is a safe and helpful response")
50+
detection: str = Field(example="vllm_model")
51+
detection_type: str = Field(example="llm_judge")
52+
score: float = Field(example=0.8)
53+
evidences: Optional[List[EvidenceObj]] = Field(
54+
description="Optional field providing evidences for the provided detection",
55+
default=[],
56+
)
57+
metadata: Optional[Dict[str, Any]] = Field(default_factory=dict, description="Additional metadata from evaluation")
58+
59+
60+
class ContentsAnalysisResponse(RootModel):
61+
root: List[List[ContentAnalysisResponse]] = Field(
62+
title="Response Text Content Analysis LLM Judge"
63+
)
64+
65+
66+
class Error(BaseModel):
67+
code: int
68+
message: str
69+
70+
71+
class MetricsListResponse(BaseModel):
72+
"""Response for listing available metrics."""
73+
metrics: List[str] = Field(description="List of available metric names")
74+
total: int = Field(description="Total number of available metrics")

0 commit comments

Comments
 (0)