You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

118 lines
4.0 KiB
Python

"""Ollama/Qwen summarizer for job postings."""
from __future__ import annotations
from typing import TYPE_CHECKING
from loguru import logger
from tenacity import retry, stop_after_attempt, wait_exponential
if TYPE_CHECKING:
from gimme_job.models.db import JobPosting
class OllamaSummarizer:
def __init__(
self,
base_url: str = "http://127.0.0.1:11434",
model: str = "qwen3.5:9b",
temperature: float = 0.1,
):
self.base_url = base_url.rstrip("/")
self.model = model
self.temperature = temperature
def summarize(self, postings: list["JobPosting"]) -> str:
"""Generate a Korean-language summary of job postings."""
if not postings:
return "오늘 신규 채용 공고가 없습니다."
prompt = self._build_prompt(postings)
try:
return self._call_ollama(prompt)
except Exception as e:
logger.error(f"Ollama summarization failed: {e}")
return self._fallback_summary(postings)
@retry(stop=stop_after_attempt(3), wait=wait_exponential(multiplier=1, min=2, max=10))
def _call_ollama(self, prompt: str) -> str:
import httpx
response = httpx.post(
f"{self.base_url}/api/generate",
json={
"model": self.model,
"prompt": prompt,
"temperature": self.temperature,
"stream": False,
"options": {"num_predict": 2048},
},
timeout=120.0,
)
response.raise_for_status()
data = response.json()
return data.get("response", "").strip()
def _build_prompt(self, postings: list["JobPosting"]) -> str:
from jinja2 import Environment, FileSystemLoader
from gimme_job.utils.paths import project_root
template_path = project_root() / "gimme_job" / "templates" / "digest.md.j2"
if template_path.exists():
env = Environment(loader=FileSystemLoader(str(template_path.parent)))
template = env.get_template("digest.md.j2")
return template.render(postings=postings, count=len(postings))
# Inline fallback
lines = []
for p in postings[:100]:
parts = [f"[{p.site_id}]", p.title]
if p.company:
parts.append(p.company)
if p.location:
parts.append(p.location)
if p.posted_text:
parts.append(p.posted_text)
if p.job_url:
parts.append(p.job_url)
lines.append(" / ".join(parts))
postings_text = "\n".join(f"- {l}" for l in lines)
return (
"다음은 오늘 수집된 신규 채용 공고 목록입니다. "
"한국어로 bullet point 요약을 작성하고, 추천 우선순위 3개를 제시하세요. "
"원본 데이터를 절대 변조하지 마세요.\n\n"
f"{postings_text}\n\n"
"형식:\n"
"# 오늘의 신규 채용 공고\n"
"- [사이트] 직무 / 회사 / 위치 / 게시일 / URL\n\n"
"## 추천 우선순위\n"
"1. ...\n"
"2. ...\n"
"3. ..."
)
def _fallback_summary(self, postings: list["JobPosting"]) -> str:
"""Plain-text fallback when Ollama is unavailable."""
lines = ["# 오늘의 신규 채용 공고\n"]
for p in postings:
parts = [f"[{p.site_id}]", p.title]
if p.company:
parts.append(p.company)
if p.location:
parts.append(p.location)
if p.posted_text:
parts.append(p.posted_text)
if p.job_url:
parts.append(p.job_url)
lines.append("- " + " / ".join(parts))
return "\n".join(lines)
def is_available(self) -> bool:
try:
import httpx
r = httpx.get(f"{self.base_url}/api/tags", timeout=3.0)
return r.status_code == 200
except Exception:
return False