Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,16 @@ OPENROUTER_API_KEY=
OPENROUTER_API_BASE=https://openrouter.ai/api/v1
OPENROUTER_APP_URL=
OPENROUTER_APP_NAME=newsletter-maker
AI_CLASSIFICATION_MODEL=meta-llama/llama-3.1-70b-instruct
AI_RELEVANCE_MODEL=qwen/qwen-2.5-72b-instruct
AI_SUMMARIZATION_MODEL=google/gemma-3-27b-it
AI_CLASSIFICATION_REVIEW_THRESHOLD=0.6
AI_RELEVANCE_LOW_THRESHOLD=0.5
AI_RELEVANCE_HIGH_THRESHOLD=0.85
AI_RELEVANCE_REVIEW_THRESHOLD=0.4
AI_RELEVANCE_SUMMARIZE_THRESHOLD=0.7
AI_MAX_NODE_RETRIES=2
AI_REQUEST_TIMEOUT_SECONDS=60
EMBEDDING_PROVIDER=sentence-transformers
EMBEDDING_MODEL=sentence-transformers/all-MiniLM-L6-v2
EMBEDDING_TRUST_REMOTE_CODE=false
Expand Down
5 changes: 4 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"noinput",
"nomic",
"OLLAMA",
"ormsgpack",
"PRAW",
"psycopg",
"pylint",
Expand All @@ -28,6 +29,8 @@
"readyz",
"Referer",
"upserted",
"upvote"
"upvote",
"uritemplate",
"xxhash"
]
}
Binary file modified celerybeat-schedule
Binary file not shown.
71 changes: 71 additions & 0 deletions core/llm.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
from __future__ import annotations

import json
import re
import time
from dataclasses import dataclass
from typing import Any

import httpx
from django.conf import settings

JSON_OBJECT_PATTERN = re.compile(r"\{.*\}", re.DOTALL)


@dataclass(slots=True)
class OpenRouterJSONResponse:
payload: dict[str, Any]
model: str
latency_ms: int


def openrouter_chat_json(*, model: str, system_prompt: str, user_prompt: str) -> OpenRouterJSONResponse:
if not settings.OPENROUTER_API_KEY:
raise RuntimeError("OPENROUTER_API_KEY must be configured for OpenRouter chat completions.")

headers = {
"Authorization": f"Bearer {settings.OPENROUTER_API_KEY}",
"Content-Type": "application/json",
}
if settings.OPENROUTER_APP_URL:
headers["HTTP-Referer"] = settings.OPENROUTER_APP_URL
if settings.OPENROUTER_APP_NAME:
headers["X-OpenRouter-Title"] = settings.OPENROUTER_APP_NAME

started_at = time.perf_counter()
response = httpx.post(
f"{settings.OPENROUTER_API_BASE.rstrip('/')}/chat/completions",
headers=headers,
json={
"model": model,
"temperature": 0,
"response_format": {"type": "json_object"},
"messages": [
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_prompt},
],
},
timeout=settings.AI_REQUEST_TIMEOUT_SECONDS,
)
latency_ms = int((time.perf_counter() - started_at) * 1000)
response.raise_for_status()

message_content = response.json()["choices"][0]["message"]["content"]
return OpenRouterJSONResponse(
payload=_extract_json_object(message_content),
model=model,
latency_ms=latency_ms,
)


def _extract_json_object(message_content: str) -> dict[str, Any]:
try:
payload = json.loads(message_content)
except json.JSONDecodeError:
match = JSON_OBJECT_PATTERN.search(message_content)
if not match:
raise ValueError("Model response did not contain a JSON object.")
payload = json.loads(match.group(0))
if not isinstance(payload, dict):
raise ValueError("Model response JSON must be an object.")
return payload
Loading
Loading