[crawler]
This commit is contained in:
2
crawler_platform/app/core/recommendation/__init__.py
Normal file
2
crawler_platform/app/core/recommendation/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
||||
"""Recommendation integration helpers."""
|
||||
|
||||
100
crawler_platform/app/core/recommendation/scorer.py
Normal file
100
crawler_platform/app/core/recommendation/scorer.py
Normal file
@@ -0,0 +1,100 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any
|
||||
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from crawler_platform.app.core.database import models
|
||||
from crawler_platform.app.core.database.repository import canonicalize
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class PreferenceInput:
|
||||
likes: list[str] = field(default_factory=list)
|
||||
dislikes: list[str] = field(default_factory=list)
|
||||
preferred_moods: list[str] = field(default_factory=list)
|
||||
preferred_notes: list[str] = field(default_factory=list)
|
||||
avoided_notes: list[str] = field(default_factory=list)
|
||||
price_preference: dict[str, Any] = field(default_factory=dict)
|
||||
season_context: str | None = None
|
||||
occasion_context: str | None = None
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class Recommendation:
|
||||
entity_id: int
|
||||
name: str
|
||||
entity_type: str
|
||||
score: float
|
||||
reasons: list[str]
|
||||
|
||||
|
||||
class RuleBasedRecommender:
|
||||
def __init__(self, session: Session):
|
||||
self.session = session
|
||||
|
||||
def recommend(
|
||||
self,
|
||||
project_id: int,
|
||||
target_entity_type: str,
|
||||
preference: PreferenceInput,
|
||||
limit: int = 10,
|
||||
) -> list[Recommendation]:
|
||||
entities = self.session.scalars(
|
||||
select(models.Entity).where(
|
||||
models.Entity.project_id == project_id,
|
||||
models.Entity.entity_type == target_entity_type,
|
||||
)
|
||||
).all()
|
||||
scored = [self._score_entity(project_id, entity, preference) for entity in entities]
|
||||
scored = [item for item in scored if item.score > 0]
|
||||
return sorted(scored, key=lambda item: item.score, reverse=True)[:limit]
|
||||
|
||||
def _score_entity(self, project_id: int, entity: models.Entity, preference: PreferenceInput) -> Recommendation:
|
||||
claims = self.session.execute(
|
||||
select(models.Claim, models.Entity)
|
||||
.join(models.Entity, models.Claim.object_entity_id == models.Entity.id, isouter=True)
|
||||
.where(models.Claim.project_id == project_id, models.Claim.subject_entity_id == entity.id)
|
||||
).all()
|
||||
score = 0.0
|
||||
reasons: list[str] = []
|
||||
preferred_notes = {canonicalize(item) for item in preference.preferred_notes}
|
||||
avoided_notes = {canonicalize(item) for item in preference.avoided_notes}
|
||||
preferred_moods = {canonicalize(item) for item in preference.preferred_moods}
|
||||
for claim, object_entity in claims:
|
||||
object_name = canonicalize(object_entity.name) if object_entity else ""
|
||||
weight = claim.confidence
|
||||
if claim.predicate in {"hasTopNote", "hasMiddleNote", "hasBaseNote", "hasScentNote", "hasFlavorNote"}:
|
||||
if object_name in preferred_notes:
|
||||
score += 2.0 * weight
|
||||
reasons.append(f"preferred note matched: {object_entity.name}")
|
||||
if object_name in avoided_notes:
|
||||
score -= 3.0 * weight
|
||||
reasons.append(f"avoided note matched: {object_entity.name}")
|
||||
if claim.predicate == "evokesMood" and object_name in preferred_moods:
|
||||
score += 1.5 * weight
|
||||
reasons.append(f"preferred mood matched: {object_entity.name}")
|
||||
if preference.season_context and claim.predicate == "suitableForSeason":
|
||||
if object_name == canonicalize(preference.season_context):
|
||||
score += 1.2 * weight
|
||||
reasons.append(f"season context matched: {preference.season_context}")
|
||||
if preference.occasion_context and claim.predicate == "suitableForOccasion":
|
||||
if object_name == canonicalize(preference.occasion_context):
|
||||
score += 1.0 * weight
|
||||
reasons.append(f"occasion context matched: {preference.occasion_context}")
|
||||
if canonicalize(entity.name) in {canonicalize(item) for item in preference.dislikes}:
|
||||
score -= 10
|
||||
reasons.append("explicit dislike")
|
||||
if canonicalize(entity.name) in {canonicalize(item) for item in preference.likes}:
|
||||
score += 5
|
||||
reasons.append("explicit like")
|
||||
return Recommendation(
|
||||
entity_id=entity.id,
|
||||
name=entity.name,
|
||||
entity_type=entity.entity_type,
|
||||
score=round(score, 4),
|
||||
reasons=reasons[:5],
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user