92 lines
3.4 KiB
Python
92 lines
3.4 KiB
Python
import os
|
|
import logging
|
|
import numpy as np
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
OPENROUTER_API_KEY = os.getenv('OPENROUTER_API_KEY')
|
|
EMBEDDING_MODEL = 'openai/text-embedding-3-small'
|
|
SIMILARITY_THRESHOLD = 0.75
|
|
|
|
|
|
def embed_trip(trip):
|
|
"""
|
|
Generates a semantic embedding for a trip using OpenRouter's
|
|
text-embedding-3-small, combining trip name, destination, duration,
|
|
and activities into a single text representation first.
|
|
|
|
Data flow:
|
|
trip dict → formatted text string → OpenRouter embeddings API →
|
|
vector list returned for storage in products.embedding /
|
|
client_trips.embedding
|
|
"""
|
|
# Imported lazily so this module has no hard dependency on the
|
|
# `openai` package at import time for callers that only need
|
|
# find_comparable_trips() (pure math, no API calls).
|
|
from openai import OpenAI
|
|
|
|
text = (
|
|
f"{trip.get('trip_name', '')}\n"
|
|
f"Destination: {trip.get('destination', '')}\n"
|
|
f"Duration: {trip.get('duration_days', '')} days\n"
|
|
f"Activities: {trip.get('activities', '')}\n"
|
|
f"Start: {trip.get('start_location', '')}"
|
|
)
|
|
|
|
client = OpenAI(api_key=OPENROUTER_API_KEY, base_url='https://openrouter.ai/api/v1')
|
|
response = client.embeddings.create(model=EMBEDDING_MODEL, input=text)
|
|
return response.data[0].embedding
|
|
|
|
|
|
def _cosine_similarity(a, b):
|
|
"""Standard cosine similarity between two equal-length embedding vectors."""
|
|
a, b = np.array(a), np.array(b)
|
|
denom = (np.linalg.norm(a) * np.linalg.norm(b))
|
|
if denom == 0:
|
|
return 0.0
|
|
return float(np.dot(a, b) / denom)
|
|
|
|
|
|
def find_comparable_trips(client_trips, competitor_trips, threshold=SIMILARITY_THRESHOLD):
|
|
"""
|
|
Calculates cosine similarity between every client trip and every
|
|
competitor trip, returning matches above threshold sorted by
|
|
similarity score descending.
|
|
|
|
Data flow:
|
|
client_trips + competitor_trips (each with .embedding) →
|
|
pairwise cosine similarity → filter by threshold →
|
|
sort by score descending → list of match dicts returned
|
|
"""
|
|
matches = []
|
|
for client_trip in client_trips:
|
|
client_embedding = client_trip.get('embedding')
|
|
if not client_embedding:
|
|
continue
|
|
for competitor_trip in competitor_trips:
|
|
competitor_embedding = competitor_trip.get('embedding')
|
|
if not competitor_embedding:
|
|
continue
|
|
score = _cosine_similarity(client_embedding, competitor_embedding)
|
|
if score >= threshold:
|
|
matches.append({
|
|
'client_product': client_trip.get('trip_name'),
|
|
'competitor_product': competitor_trip.get('id'),
|
|
'similarity_score': round(score, 4),
|
|
'price_difference': _safe_diff(
|
|
competitor_trip.get('majority_price_usd'), client_trip.get('price_usd')
|
|
),
|
|
'duration_difference': _safe_diff(
|
|
competitor_trip.get('duration_days'), client_trip.get('duration_days')
|
|
),
|
|
})
|
|
matches.sort(key=lambda m: m['similarity_score'], reverse=True)
|
|
return matches
|
|
|
|
|
|
def _safe_diff(a, b):
|
|
"""Returns a - b if both are present, else None — keeps the diff fields nullable per schema."""
|
|
if a is None or b is None:
|
|
return None
|
|
return a - b
|