File size: 5,493 Bytes
a422c4e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
from typing import List, Tuple, Dict
from langchain_core.prompts import ChatPromptTemplate
from langchain_mistralai.chat_models import ChatMistralAI
import os
from dotenv import load_dotenv

load_dotenv()

def create_ranking_chain():
    """Create a ranking chain using new RunnableSequence format"""
    prompt = ChatPromptTemplate.from_messages([
        ("system", """You are a movie recommendation expert. Your task is to select the top 10 most relevant movies from a list of recommended movies and provide the final formatted output with brief explanations.

Rules:
1. Always return exactly 10 movies
2. Consider both relevance scores and how well each movie matches user preferences
3. Pay attention to the alpha weighting parameter - it tells you how much to prioritize text preferences vs viewing history
4. Return only movies from the provided list
5. NEVER recommend movies that are already in the user's viewing history - these should be completely excluded
6. Format each movie exactly as: **1. Movie Title**\n[Exactly 2 sentences explaining why this movie matches their taste]\n\n
7. Number from 1 to 10, no additional text before or after"""),
        ("user", """Given these movie recommendations with their relevance scores:
{movie_scores}

User preferences: {preferences}

User's viewing history (DO NOT RECOMMEND ANY OF THESE): {user_movies}

Alpha weighting: {alpha}
(α=0.0 means recommendations were based entirely on viewing history, α=1.0 means entirely on text preferences, α=0.5 means equal balance)

Select the 10 most relevant movies and provide the final formatted output with explanations. Format each as:
**1. Movie Title**
[Exactly 2 sentences explaining why this movie matches their taste based on the weighted combination of their preferences and history]

**2. Movie Title**
[Exactly 2 sentences explaining why this movie matches their taste based on the weighted combination of their preferences and history]

...continue for all 10 movies.

Remember: NEVER include any movie from the user's viewing history in your recommendations.""")
    ])

    model = ChatMistralAI(
        mistral_api_key=os.environ["MISTRAL_API_KEY"],
        model="mistral-large-latest", 
        temperature=0.5,
        max_tokens=1200,
        streaming=True
    )

    return prompt | model



def rank_with_ai(recommendations: List[Tuple[str, float]], user_preferences: str = "", alpha: float = 0.5, user_movies: List[str] = None):
    """
    Complete reranking and explanation pipeline with streaming:
    1. Takes top 100 candidates from retrieval phase
    2. Reranks to top 10 using AI
    3. Generates explanations with streaming
    4. Yields partial formatted responses
    
    Args:
        recommendations: List of (movie_title, relevance_score) tuples from retrieval phase
        user_preferences: User's textual preferences/description
        alpha: Weighting parameter (0.0 = only history matters, 1.0 = only preferences matter)
        user_movies: List of user's selected movies for context
    """
    print(f"\n=== RANKING_AGENT DEBUG ===")
    print(f"Received {len(recommendations) if recommendations else 0} recommendations")
    print(f"User preferences: '{user_preferences}' (length: {len(user_preferences) if user_preferences else 0})")
    print(f"Alpha: {alpha}")
    print(f"User movies: {user_movies}")
    
    if not recommendations:
        yield "No recommendations available."
        return
    
    # Take only top 100 recommendations if more are provided
    recommendations = recommendations[:100]
    
    try:
        # Format movie scores for ranking
        movie_scores = "\n".join(
            f"{title} (relevance: {score:.3f})"
            for title, score in recommendations
        )
        
        # Start with header
        result_header = "## 🎬 Your Personalized Movie Recommendations\n\n"
        
        if user_movies and user_preferences:
            result_header += f"*Based on α={alpha} weighting: {int((1-alpha)*100)}% your viewing history + {int(alpha*100)}% your preferences*\n\n"
        elif user_preferences:
            result_header += f"*Based entirely on your preferences: \"{user_preferences}\"*\n\n"
        elif user_movies:
            result_header += f"*Based entirely on your viewing history*\n\n"
        
        result_header += "---\n\n"
        yield result_header
        
        # Single chain that does both ranking and explanation
        ranking_chain = create_ranking_chain()
        print("Calling unified ranking + explanation chain...")
        
        # Stream the response directly
        accumulated_text = result_header
        for chunk in ranking_chain.stream({
            "movie_scores": movie_scores,
            "preferences": user_preferences if user_preferences else "No specific preferences provided",
            "user_movies": ", ".join(user_movies) if user_movies else "None",
            "alpha": alpha
        }):
            if chunk.content:
                accumulated_text += chunk.content
                yield accumulated_text
            
    except Exception as e:
        print(f"ERROR in rank_with_ai: {str(e)}")
        import traceback
        traceback.print_exc()
        # Fallback to simple format
        result = "## 🎬 Your Recommendations\n\n"
        for i, (title, score) in enumerate(recommendations[:10], 1):
            result += f"**{i}. {title}**\n"
            result += f"*Similarity: {score:.3f}*\n\n"
        yield result