2026-10-14 20:35:48 +08:00
|
|
|
"""Small fixed-endpoint client for optional OpenRouter Jev relevance judgments."""
|
|
|
|
|
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
import json
|
|
|
|
|
from dataclasses import dataclass
|
|
|
|
|
from typing import Any, cast
|
|
|
|
|
from urllib.error import HTTPError, URLError
|
|
|
|
|
from urllib.request import HTTPRedirectHandler, Request, build_opener
|
|
|
|
|
|
2026-10-14 20:43:09 +08:00
|
|
|
from gewu.domain.contracts import (
|
|
|
|
|
FacetName,
|
|
|
|
|
IngestReviewCreate,
|
|
|
|
|
JevFacetScore,
|
|
|
|
|
ProjectRecord,
|
|
|
|
|
ResearchLens,
|
|
|
|
|
)
|
2026-10-14 20:35:48 +08:00
|
|
|
|
|
|
|
|
ENDPOINT = "https://openrouter.ai/api/alpha/decisions"
|
|
|
|
|
MODEL = "typesafe/jev-1.13"
|
|
|
|
|
MAX_RESPONSE_BYTES = 32_000
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
|
|
|
class JevAssessment:
|
|
|
|
|
relation_probability: float
|
|
|
|
|
lens: ResearchLens
|
|
|
|
|
lens_confidence: float
|
|
|
|
|
model: str
|
2026-10-14 20:43:09 +08:00
|
|
|
facet_scores: tuple[JevFacetScore, ...] = ()
|
2026-10-14 20:35:48 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
class JevUnavailable(RuntimeError):
|
|
|
|
|
"""Jev is not configured or returned an unusable response."""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class _NoRedirect(HTTPRedirectHandler):
|
|
|
|
|
def redirect_request(
|
|
|
|
|
self, request: Any, response: Any, code: int, message: str, headers: Any, new_url: str
|
|
|
|
|
) -> None:
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class OpenRouterJev:
|
|
|
|
|
def __init__(self, api_key: str, *, model: str = MODEL) -> None:
|
|
|
|
|
if not api_key or len(api_key) > 512 or any(char.isspace() for char in api_key):
|
|
|
|
|
raise ValueError("OpenRouter API key is invalid")
|
|
|
|
|
if model not in {MODEL, "~typesafe/jev-latest"}:
|
|
|
|
|
raise ValueError("Unsupported Jev model identifier")
|
|
|
|
|
self._api_key = api_key
|
|
|
|
|
self._model = model
|
|
|
|
|
|
|
|
|
|
def assess(self, item: IngestReviewCreate, project: ProjectRecord) -> JevAssessment:
|
2026-10-14 20:43:09 +08:00
|
|
|
facet_candidates: tuple[tuple[FacetName, tuple[str, ...]], ...] = (
|
|
|
|
|
("tags", item.candidate_tags),
|
|
|
|
|
("audience", item.candidate_audience),
|
|
|
|
|
("use_for", item.candidate_use_for),
|
|
|
|
|
("domain", item.candidate_domain),
|
|
|
|
|
)
|
|
|
|
|
facet_questions: dict[str, object] = {}
|
|
|
|
|
facet_question_map: dict[str, tuple[FacetName, str]] = {}
|
|
|
|
|
for facet, candidates in facet_candidates:
|
|
|
|
|
for index, value in enumerate(candidates):
|
|
|
|
|
key = f"facet_{facet}_{index}"
|
|
|
|
|
facet_question_map[key] = (facet, value)
|
|
|
|
|
facet_questions[key] = {
|
|
|
|
|
"type": "noul",
|
|
|
|
|
"instructions": (
|
|
|
|
|
f"Does the candidate {facet} label accurately describe the passage "
|
|
|
|
|
"in the context of this project? Judge the label itself, not just "
|
|
|
|
|
"whether its words appear in the excerpt."
|
|
|
|
|
),
|
|
|
|
|
"criteria": {
|
|
|
|
|
"true": "This label is a useful, well-supported classification.",
|
|
|
|
|
"false": "This label is inaccurate, unsupported, or too broad.",
|
|
|
|
|
},
|
|
|
|
|
}
|
2026-10-14 20:35:48 +08:00
|
|
|
state = {
|
|
|
|
|
"project_title": project.title,
|
|
|
|
|
"project_north_star": project.north_star,
|
|
|
|
|
"project_questions": project.key_questions,
|
|
|
|
|
"candidate_lens": item.lens,
|
|
|
|
|
"candidate_summary": item.summary,
|
|
|
|
|
"candidate_relevance_rationale": item.relevance_rationale,
|
|
|
|
|
"source_excerpt": item.excerpt,
|
2026-10-14 20:43:09 +08:00
|
|
|
"candidate_facets": {
|
|
|
|
|
facet: candidates for facet, candidates in facet_candidates if candidates
|
|
|
|
|
},
|
2026-10-14 20:35:48 +08:00
|
|
|
}
|
|
|
|
|
criteria = {
|
|
|
|
|
"technical": "Engineering, implementation, technology, or operational practice.",
|
|
|
|
|
"conceptual": "Ideas, theory, design concepts, or analytical frameworks.",
|
|
|
|
|
"cultural-historical": "Cultural, artistic, historical, social, or audience context.",
|
|
|
|
|
"financial-market": "Costs, revenues, business models, markets, or financing.",
|
|
|
|
|
}
|
2026-10-14 20:43:09 +08:00
|
|
|
questions: dict[str, object] = {
|
|
|
|
|
"project_relation": {
|
|
|
|
|
"type": "noul",
|
|
|
|
|
"instructions": (
|
|
|
|
|
"Does this passage help answer a project question or explain "
|
|
|
|
|
"the project's technical, conceptual, cultural-historical, or "
|
|
|
|
|
"financial-market context? Treat indirect context as relevant when "
|
|
|
|
|
"the connection is plausible."
|
|
|
|
|
),
|
|
|
|
|
"criteria": {
|
|
|
|
|
"true": "A plausible, specific connection to the project exists.",
|
|
|
|
|
"false": "The passage has no plausible project connection.",
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
"best_lens": {
|
|
|
|
|
"type": "choice",
|
|
|
|
|
"instructions": "Which project research lens best describes this passage?",
|
|
|
|
|
"criteria": criteria,
|
|
|
|
|
},
|
|
|
|
|
**facet_questions,
|
|
|
|
|
}
|
2026-10-14 20:35:48 +08:00
|
|
|
payload = json.dumps(
|
|
|
|
|
{
|
|
|
|
|
"model": self._model,
|
|
|
|
|
"state": state,
|
2026-10-14 20:43:09 +08:00
|
|
|
"questions": questions,
|
2026-10-14 20:35:48 +08:00
|
|
|
},
|
|
|
|
|
ensure_ascii=False,
|
|
|
|
|
separators=(",", ":"),
|
|
|
|
|
).encode("utf-8")
|
|
|
|
|
request = Request(
|
|
|
|
|
ENDPOINT,
|
|
|
|
|
data=payload,
|
|
|
|
|
headers={
|
|
|
|
|
"Authorization": f"Bearer {self._api_key}",
|
|
|
|
|
"Content-Type": "application/json",
|
|
|
|
|
"Accept": "application/json",
|
|
|
|
|
},
|
|
|
|
|
method="POST",
|
|
|
|
|
)
|
|
|
|
|
try:
|
|
|
|
|
with build_opener(_NoRedirect()).open(request, timeout=12) as response:
|
|
|
|
|
if response.status != 200:
|
|
|
|
|
raise JevUnavailable("Jev request failed")
|
|
|
|
|
raw = response.read(MAX_RESPONSE_BYTES + 1)
|
|
|
|
|
except (HTTPError, URLError, TimeoutError, OSError) as error:
|
|
|
|
|
raise JevUnavailable("Jev request failed") from error
|
|
|
|
|
if len(raw) > MAX_RESPONSE_BYTES:
|
|
|
|
|
raise JevUnavailable("Jev response exceeded its size limit")
|
|
|
|
|
try:
|
|
|
|
|
result = json.loads(raw)
|
|
|
|
|
answers = result["answers"]
|
|
|
|
|
relation = float(answers["project_relation"]["noul"])
|
|
|
|
|
lens = answers["best_lens"]["choice"]
|
|
|
|
|
confidence = float(answers["best_lens"]["confidence"])
|
|
|
|
|
served_model = str(result["model"])
|
2026-10-14 20:43:09 +08:00
|
|
|
facet_scores = tuple(
|
|
|
|
|
JevFacetScore(
|
|
|
|
|
facet=facet,
|
|
|
|
|
value=value,
|
|
|
|
|
probability=float(answers[key]["noul"]),
|
|
|
|
|
)
|
|
|
|
|
for key, (facet, value) in facet_question_map.items()
|
|
|
|
|
)
|
2026-10-14 20:35:48 +08:00
|
|
|
except (KeyError, TypeError, ValueError, json.JSONDecodeError) as error:
|
|
|
|
|
raise JevUnavailable("Jev response did not match its typed decision schema") from error
|
|
|
|
|
if (
|
|
|
|
|
not 0 <= relation <= 1
|
|
|
|
|
or not 0 <= confidence <= 1
|
|
|
|
|
or lens not in criteria
|
|
|
|
|
or len(served_model) > 100
|
|
|
|
|
):
|
|
|
|
|
raise JevUnavailable("Jev response contained an invalid decision")
|
2026-10-14 20:43:09 +08:00
|
|
|
return JevAssessment(
|
|
|
|
|
relation, cast(ResearchLens, lens), confidence, served_model, facet_scores
|
|
|
|
|
)
|