[crawler]

This commit is contained in:
LASTA_DEV01\lasta
2026-05-08 17:41:15 +09:00
commit 4158789869
54 changed files with 4355 additions and 0 deletions

View File

@@ -0,0 +1,2 @@
"""Recommendation integration helpers."""

View 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],
)