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