Pushkar02-n commited on
Commit
f69a6fa
·
1 Parent(s): f2cb2b4

Add Reranker and Optimized Embeddings and metadata by connecting everything with postgres

Browse files
pyproject.toml CHANGED
@@ -5,7 +5,7 @@ description = "Add your description here"
5
  readme = "README.md"
6
  requires-python = ">=3.11,<3.14"
7
  dependencies = [
8
- "chromadb>=0.5.0",
9
  "fastapi[all]>=0.122.0",
10
  "gradio>=6.2.0",
11
  "langchain>=1.1.3",
 
5
  readme = "README.md"
6
  requires-python = ">=3.11,<3.14"
7
  dependencies = [
8
+ "chromadb>=1.5.2",
9
  "fastapi[all]>=0.122.0",
10
  "gradio>=6.2.0",
11
  "langchain>=1.1.3",
src/api/main.py CHANGED
@@ -62,7 +62,9 @@ async def get_recommendations(request: RecommendationRequest, fastapi_req: Reque
62
  {
63
  "query": "Anime similar to Death Note but lighter",
64
  "n_results": 5,
65
- "min_score": 7.5
 
 
66
  }
67
  ```
68
  """
@@ -77,6 +79,9 @@ async def get_recommendations(request: RecommendationRequest, fastapi_req: Reque
77
 
78
  if request.genre_filter:
79
  filters["genre_filter"] = request.genre_filter
 
 
 
80
 
81
  start_time = time.time()
82
  result = rag_pipeline.recommend(
@@ -86,12 +91,13 @@ async def get_recommendations(request: RecommendationRequest, fastapi_req: Reque
86
  end_time = time.time()
87
 
88
  # print(f"Retrieved anime : \n{result["retrieved_animes"][0]}")
89
- print(f"Retrieved anime Count : \n{result["retrieved_count"]}")
90
  print(f"Result Recommendations: \n{result["recommendations"][:20]}")
91
  return RecommendationResponse(
92
  query=result["query"],
93
  recommendations=result["recommendations"],
94
- retrieved_count=result["retrieved_count"],
 
95
  metadata={
96
  "model": settings.model_name,
97
  "retriever_k": rag_pipeline.retriever_k,
 
62
  {
63
  "query": "Anime similar to Death Note but lighter",
64
  "n_results": 5,
65
+ "min_score": 7.5,
66
+ "genre_filter": ["Comedy", "Fantasy"],
67
+ "anime_type": TV
68
  }
69
  ```
70
  """
 
79
 
80
  if request.genre_filter:
81
  filters["genre_filter"] = request.genre_filter
82
+
83
+ if request.anime_type:
84
+ filters["anime_type"] = request.anime_type
85
 
86
  start_time = time.time()
87
  result = rag_pipeline.recommend(
 
91
  end_time = time.time()
92
 
93
  # print(f"Retrieved anime : \n{result["retrieved_animes"][0]}")
94
+ print(f"Retrieved anime Count : \n{result["reranked_count"]}")
95
  print(f"Result Recommendations: \n{result["recommendations"][:20]}")
96
  return RecommendationResponse(
97
  query=result["query"],
98
  recommendations=result["recommendations"],
99
+ retrieved_count=result["reranked_count"],
100
+ retrieved_animes=result["retrieved_animes"],
101
  metadata={
102
  "model": settings.model_name,
103
  "retriever_k": rag_pipeline.retriever_k,
src/api/schemas.py CHANGED
@@ -7,11 +7,13 @@ class RecommendationRequest(BaseModel):
7
  5, description="Number of recommendations to return")
8
  min_score: float | None = Field(
9
  None, description="Minimum MyAnimeList Score filter")
10
- genre_filter: str | None = Field(None, description="Filter by genre")
 
11
 
12
 
13
  class RecommendationResponse(BaseModel):
14
  query: str
15
  recommendations: str
16
  retrieved_count: int
 
17
  metadata: dict = {}
 
7
  5, description="Number of recommendations to return")
8
  min_score: float | None = Field(
9
  None, description="Minimum MyAnimeList Score filter")
10
+ genre_filter: list[str] | None = Field(None, description="Filter by genre")
11
+ anime_type: str | None = Field(None, description="Filter by type(TV, Movie, etc)")
12
 
13
 
14
  class RecommendationResponse(BaseModel):
15
  query: str
16
  recommendations: str
17
  retrieved_count: int
18
+ retrieved_animes: list[dict]
19
  metadata: dict = {}
src/data_ingestion/create_embeddings.py CHANGED
@@ -1,11 +1,15 @@
1
- import pandas as pd
2
  from sentence_transformers import SentenceTransformer
3
  import chromadb
4
  from chromadb.config import Settings
5
- import os
6
  import logging
7
 
 
 
 
8
  logger = logging.getLogger(__name__)
 
 
9
 
10
 
11
  class EmbeddingPipeline:
@@ -50,6 +54,14 @@ class EmbeddingPipeline:
50
 
51
  return collection
52
 
 
 
 
 
 
 
 
 
53
  def embed_texts(self, texts: list[str], batch_size: int = 32) -> list[list[float]] | None:
54
  """
55
  Create embeddings for texts
@@ -74,62 +86,73 @@ class EmbeddingPipeline:
74
  else:
75
  logger.info(f"Using existing stored embeddings.")
76
 
77
- def store_in_chromadb(self, collection, df: pd.DataFrame, embeddings: list[list[float]]):
78
  """
79
  Store embeddings and metadata in ChromaDB
80
 
81
  Args:
82
  collection: ChromaDB collection,
83
- df: DataFrame with anime data,
84
  embeddings: Pre_commputed embeddings
85
  """
86
 
87
  logger.info("Storing in ChromaDB...")
88
 
89
- ids = [str(mal_id) for mal_id in df["mal_id"].tolist()]
90
- documents = df["searchable_text"].tolist()
91
-
92
- # Metadata
93
  metadatas = []
94
- for _, row in df.iterrows():
95
- metadata = {
96
- "title": row["title"],
97
- "genres": row["genres"],
98
- "score": float(row["score"]) if pd.notna(row["score"]) else 0.0,
99
- "type": row["type"] if pd.notna(row["type"]) else "Unknown",
100
- "year": int(row["year"]) if pd.notna(row["year"]) else 0,
101
- "synopsis": row["synopsis"][:500],
102
- "rating": row["rating"] if pd.notna(row["rating"]) else "Rating Unspecified",
103
- "scored_by": row['scored_by'],
104
- }
105
- metadatas.append(metadata)
106
-
107
- collection.add(
108
- ids=ids,
109
- embeddings=embeddings,
110
- documents=documents,
111
- metadatas=metadatas
112
- )
113
-
114
- logger.info(f"Stored {len(ids)} animes in ChromaDB")
115
- logger.info(f"Collection now has {collection.count()} documents")
116
-
117
- def run_pipeline(self, csv_path: str = 'data/processed/anime_clean.csv'):
 
 
 
 
 
 
 
 
 
 
 
 
118
  """Run complete embedding pipeline"""
119
 
120
- logger.info("Loading processed data...")
121
- df = pd.read_csv(csv_path)
122
- logger.info(f"Loaded {len(df)} animes")
123
 
124
  collection = self.create_or_get_collection()
125
 
126
- if self.use_existing_embeddings == False:
127
- embeddings = self.embed_texts(df["searchable_text"].tolist())
128
- if embeddings:
129
- self.store_in_chromadb(collection, df, embeddings)
130
 
131
- logger.info("Embedding pipeline complete !")
 
132
 
 
133
  return collection
134
 
135
 
@@ -138,13 +161,13 @@ if __name__ == "__main__":
138
  collection = pipeline.run_pipeline()
139
 
140
  print("\n--- Testing vector search ---")
141
- query = "Anime with female main character on adventure"
142
 
143
  print(f"Query: {query}")
144
 
145
  results = collection.query(query_texts=[query], n_results=15)
146
 
147
- print("\n--- TOP 5 RESULTS ---")
148
 
149
  for i, (title, distance) in enumerate(zip(
150
  [m["title"] for m in results["metadatas"][0]],
 
 
1
  from sentence_transformers import SentenceTransformer
2
  import chromadb
3
  from chromadb.config import Settings
4
+ from sqlmodel import Session, select
5
  import logging
6
 
7
+ from src.database.session import engine
8
+ from src.database.models import Animes
9
+
10
  logger = logging.getLogger(__name__)
11
+ logging.basicConfig(level=logging.INFO,
12
+ format='%(asctime)s - %(levelname)s - %(message)s')
13
 
14
 
15
  class EmbeddingPipeline:
 
54
 
55
  return collection
56
 
57
+ def fetch_data_from_postgres(self):
58
+ """Fetch all anime records from PostgreSQL"""
59
+ logger.info("Fetching data from PostgreSQL...")
60
+ with Session(engine) as session:
61
+ statement = select(Animes).where(Animes.searchable_text != None)
62
+ results = session.exec(statement).all()
63
+ return results
64
+
65
  def embed_texts(self, texts: list[str], batch_size: int = 32) -> list[list[float]] | None:
66
  """
67
  Create embeddings for texts
 
86
  else:
87
  logger.info(f"Using existing stored embeddings.")
88
 
89
+ def store_in_chromadb(self, collection, db_records: list[Animes], embeddings: list[list[float]]):
90
  """
91
  Store embeddings and metadata in ChromaDB
92
 
93
  Args:
94
  collection: ChromaDB collection,
95
+ db_records: List of Anime data retrieved from PostgreSQL database,
96
  embeddings: Pre_commputed embeddings
97
  """
98
 
99
  logger.info("Storing in ChromaDB...")
100
 
101
+ ids = []
102
+ documents = []
 
 
103
  metadatas = []
104
+
105
+ for row in db_records:
106
+ ids.append(str(row.mal_id))
107
+ documents.append(row.searchable_text)
108
+
109
+ # genres_str = ", ".join(row.genres) if isinstance(
110
+ # row.genres, list) else str(row.genres or "")
111
+ genres_list = row.genres if isinstance(row.genres, list) else []
112
+ if len(genres_list) == 0:
113
+ genres_list = ["Unknown"]
114
+
115
+ metadatas.append({
116
+ "title": row.title,
117
+ "genres": genres_list,
118
+ "score": float(row.score) if row.score else 0.0,
119
+ "type": row.type if row.type else "Unknown",
120
+ "scored_by": row.scored_by if row.scored_by else 0
121
+ })
122
+ print(f"Genre List saved: {", ".join(genres_list)}")
123
+
124
+ chunk_size = 500
125
+ total_chunks = (len(ids) // chunk_size) + 1
126
+ logger.info(f"Inserting into ChromaDB in {total_chunks} batches...")
127
+
128
+ for i in range(0, len(ids), chunk_size):
129
+ collection.add(
130
+ ids=ids[i: i + chunk_size],
131
+ embeddings=embeddings[i: i + chunk_size],
132
+ documents=documents[i: i + chunk_size],
133
+ metadatas=metadatas[i: i + chunk_size]
134
+ )
135
+ logger.info(f"Inserted batch {(i//chunk_size)+1}/{total_chunks}")
136
+
137
+ logger.info(f"✅ Successfully stored {len(ids)} animes in ChromaDB")
138
+
139
+ def run_pipeline(self):
140
  """Run complete embedding pipeline"""
141
 
142
+ # 1. Fetch from DB instead of CSV
143
+ db_records = self.fetch_data_from_postgres()
144
+ logger.info(f"Loaded {len(db_records)} animes from Postgres")
145
 
146
  collection = self.create_or_get_collection()
147
 
148
+ if not self.use_existing_embeddings:
149
+ texts_to_embed = [record.searchable_text for record in db_records]
150
+ embeddings = self.embed_texts(texts_to_embed)
 
151
 
152
+ if embeddings:
153
+ self.store_in_chromadb(collection, db_records, embeddings)
154
 
155
+ logger.info("Embedding pipeline complete!")
156
  return collection
157
 
158
 
 
161
  collection = pipeline.run_pipeline()
162
 
163
  print("\n--- Testing vector search ---")
164
+ query = "Anime similar to Attack Titan"
165
 
166
  print(f"Query: {query}")
167
 
168
  results = collection.query(query_texts=[query], n_results=15)
169
 
170
+ print("\n--- TOP 15 RESULTS ---")
171
 
172
  for i, (title, distance) in enumerate(zip(
173
  [m["title"] for m in results["metadatas"][0]],
src/llm/anime_reranker.py ADDED
@@ -0,0 +1,114 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import pandas as pd
2
+ from sentence_transformers import CrossEncoder
3
+ from sklearn.preprocessing import MinMaxScaler
4
+
5
+
6
+ class AnimeReranker:
7
+ def __init__(self, model_name: str = 'cross-encoder/ms-marco-MiniLM-L-6-v2', device: str = 'cpu'):
8
+ """
9
+ Initializes the reranker, loading the PyTorch model into memory once.
10
+ """
11
+ print(f"Loading CrossEncoder model '{model_name}' on {device}...")
12
+ self.reranker = CrossEncoder(model_name, device=device)
13
+ self.scaler = MinMaxScaler()
14
+
15
+ # ----Postgres Global Truth
16
+ self.C = 7.676 # The true weighted average score
17
+ # 50th percentile of votes (Confidence threshold)
18
+ self.bayesian_m = 10875
19
+
20
+ # Hyperparameters for scoring
21
+ self.passion_weight = 50 # Scaling factor for favorites/scored_by
22
+ self.alpha = 0.75 # Weight for Semantic Relevance
23
+ self.beta = 0.25 # Weight for Objective Quality
24
+
25
+ def _calculate_quality_score(self, df: pd.DataFrame) -> pd.DataFrame:
26
+ """Internal method to calculate the objective quality of the anime."""
27
+ # Calculate Bayesian Average
28
+ c = self.C
29
+ v = df['scored_by']
30
+ r = df['score']
31
+ df['bayesian_score'] = (v / (v + self.bayesian_m)) * \
32
+ r + (self.bayesian_m / (v + self.bayesian_m)) * c
33
+
34
+ # Calculate Passion Rate
35
+ df['passion_rate'] = df['favorites'] / (df['scored_by'] + 1)
36
+
37
+ # Combine into raw quality score
38
+ df['raw_quality_score'] = df['bayesian_score'] + \
39
+ (df['passion_rate'] * self.passion_weight)
40
+ return df
41
+
42
+ # def process(self, user_query: str, retrieved_anime: list[dict], top_k: int = 25) -> tuple[str, pd.DataFrame]:
43
+ def process(self, user_query: str, retrieved_anime: list[dict], top_k: int = 25) -> pd.DataFrame:
44
+ """
45
+ Public method to rerank, filter, and format the final context for the LLM.
46
+ """
47
+ if not retrieved_anime:
48
+ return pd.DataFrame()
49
+
50
+ df = pd.DataFrame(retrieved_anime)
51
+
52
+ # 1. Semantic Score (Cross-Encoder)
53
+ cross_input = [[user_query, text] for text in df['searchable_text']]
54
+ df['semantic_score'] = self.reranker.predict(cross_input)
55
+
56
+ # 2. Quality Score (Custom Math)
57
+ df = self._calculate_quality_score(df)
58
+
59
+ # 3. Normalize & Combine
60
+ df[['norm_semantic', 'norm_quality']] = self.scaler.fit_transform(
61
+ df[['semantic_score', 'raw_quality_score']])
62
+ df['final_hybrid_score'] = (
63
+ self.alpha * df['norm_semantic']) + (self.beta * df['norm_quality'])
64
+
65
+ # 4. Sort and Filter
66
+ df_sorted = df.sort_values(
67
+ by='final_hybrid_score', ascending=False).head(top_k)
68
+
69
+ # # 5. Format for LLM
70
+ # llm_context = "Here are the top retrieved anime matches:\n\n"
71
+ # for _, row in df_sorted.iterrows():
72
+ # llm_context += f"Title: {row['title']} (English: {row.get('title_english', 'N/A')})\n"
73
+ # llm_context += f"Genres: {row.get('genres', 'N/A')}\n"
74
+ # llm_context += f"Synopsis: {row.get('synopsis', 'N/A')}\n"
75
+ # llm_context += "-" * 40 + "\n"
76
+
77
+ # return llm_context, df_sorted
78
+ return df_sorted
79
+
80
+
81
+ # ==========================================
82
+ # USAGE IN PRODUCTION
83
+ # ==========================================
84
+ # 1. Initialize ONCE during app startup
85
+ if __name__ == "__main__":
86
+ from src.retrieval.vector_search import AnimeRetriever
87
+ reranker_service = AnimeReranker()
88
+ retriever = AnimeRetriever()
89
+
90
+ user_query = "Modern Romantic Comedy Anime"
91
+ vector_db_results = retriever.search(query=user_query,
92
+ n_results=50)
93
+
94
+ print("BEFORE RERANKING")
95
+ print("--------------------------------------------------")
96
+ for res in vector_db_results:
97
+ print(
98
+ f"Title: {res["title"]} (Score: {res["score"]}, Scored by: {res["scored_by"]}) .... Favorites={res["favorites"]}")
99
+
100
+ # 2. Call the `.process()` method during runtime (e.g., inside an API endpoint)
101
+ df = reranker_service.process(user_query,
102
+ vector_db_results,
103
+ top_k=10)
104
+ df_dict_list = df.to_dict(orient="records") if not df.empty else []
105
+ print("\nAFTER RERANKING")
106
+ print("--------------------------------------------------")
107
+ for res in df_dict_list:
108
+ print(
109
+ f"Title: {res["title"]} (Score: {res["score"]}, Scored by: {res["scored_by"]}) Favorites={res["favorites"]}")
110
+ print(f"-> Has Raw Quality Score: {res["raw_quality_score"]}")
111
+ print(f"-> Has Final normalized Quality Score: {res["norm_quality"]}")
112
+ print(f"-> Has Raw Semantic Score: {res["semantic_score"]}")
113
+ print(f"-> Has Final Semantic Score: {res["norm_semantic"]}")
114
+ print(f"-> Has Final Hybrid Score: {res["final_hybrid_score"]}\n")
src/llm/prompts.py CHANGED
@@ -35,54 +35,57 @@ You are a strict Gatekeeper for an Anime Recommendation System. Your ONLY job is
35
  If you are less than 90% sure what the user wants, DO NOT call the tool. Ask for clarification instead.
36
  """
37
 
 
38
  def create_recommendation_prompt(
39
  user_query: str,
40
  retrieved_animes: list,
41
  n_recommendations: int | None = 5
42
  ):
43
  """
44
- Create prompt for LLM to reason about recommendations
45
-
46
- Args:
47
- user_query: Original user question,
48
- retrieved_animes: List of dict from vector search,
49
- n_recommendations: How many to recommend
50
  """
51
 
52
  context_parts = []
53
- for i, anime in enumerate(retrieved_animes, 1):
54
- context_parts.append(
55
- f"{i}. **{anime['title']}** (Score: {anime['score']}/10, Scored by: {anime['scored_by']})\n"
56
- f" Genres: {anime['genres']}\n"
57
- f" Synopsis: {anime['synopsis']}\n"
 
 
 
58
  )
 
59
 
60
  context = "\n".join(context_parts)
61
 
62
  prompt = f"""
63
- You are an expert, polite, casual and friendly anime recommender. Your goal is to give personalized anime recommendations based strictly on the provided database context.
64
 
65
  User's Query: "{user_query}"
66
 
67
- Available Anime Context (from vector search):
68
  {context}
69
 
 
 
 
 
70
  # Core Directives:
71
- 1. **Curate, Don't List**: Do not just repeat the context. Analyze the user's specific vibe/theme requests and pick the top {n_recommendations} absolute best matches.
72
- 2. **THE SEQUEL RULE**: You MUST NOT recommend direct sequels, prequels, or movies of the exact anime the user mentioned, UNLESS they explicitly ask for a watch order or sequel. (e.g., If they ask for "shows like Bleach", do NOT recommend "Bleach: Thousand-Year Blood War").
73
- 3. **Conversational Tone**: Speak like a relaxed anime fan chatting with a friend. DO NOT use robotic transitions like "Based on the provided context..." or "Here are your recommendations...". Just jump right into the good stuff.
74
- 4. **The Pitch**: For each pick, write 1-2 sentences explaining exactly *why* it fits their specific request based on the synopsis and genres.
75
- 5. **No Hallucinations**: If an anime lacks a synopsis, do not invent one. Explain it using its genres and your general knowledge of its themes.
76
- 6. **Honesty**: If the retrieved context doesn't have any genuinely good matches for the user's query, be honest. Tell them the database didn't have a perfect fit and suggest what kind of show they should look for instead.
77
 
78
  # Required Format:
79
  **[Anime Title]**
80
- [Your 1-2 sentence pitch on why it fits the user's exact vibe]
81
 
82
  **[Anime Title]**
83
  [Your pitch...]
84
 
85
- (Limit to {n_recommendations} recommendations maximum.)
86
  """
87
 
88
  return prompt
 
35
  If you are less than 90% sure what the user wants, DO NOT call the tool. Ask for clarification instead.
36
  """
37
 
38
+
39
  def create_recommendation_prompt(
40
  user_query: str,
41
  retrieved_animes: list,
42
  n_recommendations: int | None = 5
43
  ):
44
  """
45
+ Create prompt for LLM to reason about recommendations with strict anti-hallucination guardrails.
 
 
 
 
 
46
  """
47
 
48
  context_parts = []
49
+ for i, anime in enumerate(retrieved_animes):
50
+ # We pick the most 'semantic' fields for the LLM to reason with
51
+ block = (
52
+ f"Title: {anime['title']} ({anime['year']})\n"
53
+ f"Type: {anime['type']} | Score: {anime['score']} | Studio: {', '.join(anime['studios'] if isinstance(anime['studios'], list) else [anime.get('studios', 'Unknown')])}\n"
54
+ f"Genres: {', '.join(anime['genres'] if isinstance(anime['genres'], list) else [anime.get('genres', '')])} | Themes: {', '.join(anime['themes'] if isinstance(anime['themes'], list) else [anime.get('themes', '')])}\n"
55
+ f"Synopsis: {anime.get('synopsis', 'No synopsis available.')}\n"
56
+ f"---"
57
  )
58
+ context_parts.append(block)
59
 
60
  context = "\n".join(context_parts)
61
 
62
  prompt = f"""
63
+ You are an expert, casual, and friendly anime recommender. Your goal is to give personalized anime recommendations based STRICTLY on the provided database context.
64
 
65
  User's Query: "{user_query}"
66
 
67
+ Available Anime Context (from vector search & reranker):
68
  {context}
69
 
70
+ IF THE CONTEXT IS EMPTY:
71
+ Politely say you cannot find any matching anime in your current database. DO NOT make up recommendations.
72
+
73
+ ELSE:
74
  # Core Directives:
75
+ 1. Quality Over Quantity (CRITICAL): You are asked to provide up to {n_recommendations} recommendations. However, if only 1 or 2 anime in the context genuinely fit the user's specific vibe/theme request, ONLY recommend those. Do NOT force connections or hallucinate similarities just to reach the maximum number.
76
+ 2. The Sequel Rule: You MUST NOT recommend direct sequels, prequels, recap movies, or specials of the exact anime the user mentioned in their query, UNLESS they explicitly ask for a watch order or more of that exact show.
77
+ 3. Ruthless Curation: If the user asks for a specific contrast (e.g., "like Death Note but funny"), actively ignore anime in the context that do not fit that contrast.
78
+ 4. Conversational Tone: Speak like a relaxed, highly knowledgeable anime fan chatting with a friend. Never use robotic transitions like "Based on the provided context..." or "Here are your recommendations."
79
+ 5. The Pitch: For each pick, write a punchy 2-3 sentence pitch explaining exactly *why* it fits their request. Use specific plot points, themes, or the studio from the provided synopsis to back up your claim.
 
80
 
81
  # Required Format:
82
  **[Anime Title]**
83
+ [Your 2-3 sentence pitch on why it fits the user's exact vibe, drawing directly from the synopsis or themes.]
84
 
85
  **[Anime Title]**
86
  [Your pitch...]
87
 
88
+ (Limit to {n_recommendations} recommendations maximum, but fewer is completely fine if the matches are weak.)
89
  """
90
 
91
  return prompt
src/retrieval/rag_pipeline.py CHANGED
@@ -1,5 +1,6 @@
1
  import json
2
  from src.retrieval.vector_search import AnimeRetriever
 
3
  from src.llm.groq_client import GroqLLM
4
  from src.llm.prompts import create_recommendation_prompt, create_system_prompt, ANIME_SEARCH_TOOL, ROUTER_SYSTEM_PROMPT
5
  import logging
@@ -12,8 +13,9 @@ class AnimeRAGPipeline:
12
  def __init__(
13
  self,
14
  retriever: AnimeRetriever | None = None,
 
15
  llm: GroqLLM | None = None,
16
- retriever_k: int = 10,
17
  recommendation_n: int | None = 5
18
  ):
19
  """
@@ -26,12 +28,14 @@ class AnimeRAGPipeline:
26
  recommendation_n: How many to recommend in final output
27
  """
28
  self.retriever = retriever or AnimeRetriever()
 
29
  self.llm = llm or GroqLLM(model=settings.model_name)
30
  self.retriever_k = retriever_k
31
  self.recommendation_n = recommendation_n
32
 
33
  logger.info("RAG Pipeline initialized")
34
  logger.info(f" - Retrieve top {retriever_k} anime from vector search")
 
35
  logger.info(
36
  f" - LLM reasons and recommends top {recommendation_n} anime")
37
 
@@ -45,7 +49,7 @@ class AnimeRAGPipeline:
45
 
46
  Args:
47
  user_query: User's request (e.g., "Anime like death note for lighter")
48
- filters: Optional filters (min_score, genre_filter)
49
 
50
  Returns:
51
  Dict with:
@@ -58,7 +62,7 @@ class AnimeRAGPipeline:
58
  filters = filters or {}
59
 
60
  # [STEP 1] The Agentic Decision Call
61
- logger.info("[1/4] Asking LLM if it needs to search...")
62
 
63
  initial_response = self.llm.chat_with_tools(
64
  messages=[{"role": "user", "content": user_query}],
@@ -77,7 +81,7 @@ class AnimeRAGPipeline:
77
  }
78
  if not initial_response.tool_calls:
79
  logger.info(
80
- "[2/4] No search needed. Returning conversational response.")
81
  return {
82
  "query": user_query,
83
  "retrieved_count": 0,
@@ -86,7 +90,7 @@ class AnimeRAGPipeline:
86
  }
87
 
88
  # [STEP 3] The LLM wants to search. Extract its optimized parameters.
89
- logger.info("[2/4] Tool called! Executing vector search...")
90
  tool_call = initial_response.tool_calls[0]
91
  tool_args = json.loads(tool_call.function.arguments)
92
 
@@ -94,21 +98,40 @@ class AnimeRAGPipeline:
94
 
95
  optimized_query = tool_args.get("optimized_query", user_query)
96
 
 
97
  retrieved_animes = self.retriever.search(
98
  query=optimized_query,
99
  n_results=self.retriever_k,
100
  **filters
101
  )
102
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
103
  # [STEP 4] The Final Recommendation Call
104
- logger.info("[3/4] Creating prompt with retrieved content...")
105
  prompt = create_recommendation_prompt(
106
  user_query=user_query,
107
- retrieved_animes=retrieved_animes,
108
  n_recommendations=self.recommendation_n
109
  )
110
 
111
- logger.info("[4/4] LLM generating final response...")
112
  system_prompt = create_system_prompt()
113
 
114
  recommendations = self.llm.generate(
@@ -123,8 +146,9 @@ class AnimeRAGPipeline:
123
  return {
124
  "query": user_query,
125
  "retrieved_count": len(retrieved_animes),
 
126
  "recommendations": recommendations,
127
- "retrieved_animes": retrieved_animes
128
  }
129
 
130
  def recommend_streaming(self, user_query: str, filters: dict | None = None):
@@ -137,40 +161,79 @@ class AnimeRAGPipeline:
137
 
138
  if __name__ == "__main__":
139
  import json
 
 
 
 
140
 
141
  pipeline = AnimeRAGPipeline(
142
- retriever_k=10,
143
  recommendation_n=5
144
  )
145
 
146
  test_queries = [
147
  "Anime similar to Death Note but lighter in tone",
148
- "Romantic comedy set in high school",
149
- "Dark psychological thriller",
150
- "Action anime with great animation and epic fights"
151
  ]
152
 
153
  for query in test_queries:
154
- result = pipeline.recommend(user_query=query)
 
 
155
 
156
- print(f"-------\nQuery: {query}:::\n-----")
157
-
158
- print(f"\nRecommendations:\n---------------")
159
-
160
- print(result["recommendations"])
161
-
162
- # Save results for inspection
163
- filename = f"test_result_{query[:30].replace(' ', '_')}.json"
164
- with open(f"data/{filename}", "w") as f:
165
- json.dump({
166
- "query": result["query"],
167
- "retrieved_titles": [a["title"] for a in result["retrieved_animes"]],
168
- "recommendations": result["recommendations"]
169
- }, f, indent=2)
170
 
171
- print(f"\nSaved to data/{filename}")
172
- print("\n" + "="*80 + "\n")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
173
 
174
- # Pause between queries to respect rate limits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
175
  import time
176
- time.sleep(2)
 
1
  import json
2
  from src.retrieval.vector_search import AnimeRetriever
3
+ from src.llm.anime_reranker import AnimeReranker
4
  from src.llm.groq_client import GroqLLM
5
  from src.llm.prompts import create_recommendation_prompt, create_system_prompt, ANIME_SEARCH_TOOL, ROUTER_SYSTEM_PROMPT
6
  import logging
 
13
  def __init__(
14
  self,
15
  retriever: AnimeRetriever | None = None,
16
+ reranker: AnimeReranker | None = None,
17
  llm: GroqLLM | None = None,
18
+ retriever_k: int = 50,
19
  recommendation_n: int | None = 5
20
  ):
21
  """
 
28
  recommendation_n: How many to recommend in final output
29
  """
30
  self.retriever = retriever or AnimeRetriever()
31
+ self.reranker = reranker or AnimeReranker()
32
  self.llm = llm or GroqLLM(model=settings.model_name)
33
  self.retriever_k = retriever_k
34
  self.recommendation_n = recommendation_n
35
 
36
  logger.info("RAG Pipeline initialized")
37
  logger.info(f" - Retrieve top {retriever_k} anime from vector search")
38
+ logger.info(f" - Rerank and filter using Cross-Encoder & Bayesian Math")
39
  logger.info(
40
  f" - LLM reasons and recommends top {recommendation_n} anime")
41
 
 
49
 
50
  Args:
51
  user_query: User's request (e.g., "Anime like death note for lighter")
52
+ filters: Optional filters (min_score, genre_filter, anime_type)
53
 
54
  Returns:
55
  Dict with:
 
62
  filters = filters or {}
63
 
64
  # [STEP 1] The Agentic Decision Call
65
+ logger.info("[1/5] Asking LLM if it needs to search...")
66
 
67
  initial_response = self.llm.chat_with_tools(
68
  messages=[{"role": "user", "content": user_query}],
 
81
  }
82
  if not initial_response.tool_calls:
83
  logger.info(
84
+ "[2/5] No search needed. Returning conversational response.")
85
  return {
86
  "query": user_query,
87
  "retrieved_count": 0,
 
90
  }
91
 
92
  # [STEP 3] The LLM wants to search. Extract its optimized parameters.
93
+ logger.info("[2/5] Tool called! Executing vector search...")
94
  tool_call = initial_response.tool_calls[0]
95
  tool_args = json.loads(tool_call.function.arguments)
96
 
 
98
 
99
  optimized_query = tool_args.get("optimized_query", user_query)
100
 
101
+ # 3A: Fetch Top >=50 Anime from Chromadb
102
  retrieved_animes = self.retriever.search(
103
  query=optimized_query,
104
  n_results=self.retriever_k,
105
  **filters
106
  )
107
 
108
+ # 3B: Rerank the Top 50 Using Pytorch + Math
109
+ logger.info("[3/5] Reranking results with Cross-Encoder")
110
+
111
+ reranked_df = self.reranker.process(
112
+ user_query=optimized_query,
113
+ retrieved_anime=retrieved_animes,
114
+ top_k=15
115
+ )
116
+
117
+ top_animes_list = reranked_df.to_dict(
118
+ orient="records") if not reranked_df.empty else []
119
+ print("FIRST IN TOP ANIMES LIST AFTER RERANKING \n",
120
+ top_animes_list[0])
121
+ print("SECOND IN TOP ANIMES LIST AFTER RERANKING \n",
122
+ top_animes_list[1])
123
+ print(
124
+ f"After reranking, fetched {len(top_animes_list)} top animes....")
125
+
126
  # [STEP 4] The Final Recommendation Call
127
+ logger.info("[4/5] Creating prompt with retrieved content...")
128
  prompt = create_recommendation_prompt(
129
  user_query=user_query,
130
+ retrieved_animes=top_animes_list,
131
  n_recommendations=self.recommendation_n
132
  )
133
 
134
+ logger.info("[5/5] LLM generating final response...")
135
  system_prompt = create_system_prompt()
136
 
137
  recommendations = self.llm.generate(
 
146
  return {
147
  "query": user_query,
148
  "retrieved_count": len(retrieved_animes),
149
+ "reranked_count": len(top_animes_list),
150
  "recommendations": recommendations,
151
+ "retrieved_animes": top_animes_list
152
  }
153
 
154
  def recommend_streaming(self, user_query: str, filters: dict | None = None):
 
161
 
162
  if __name__ == "__main__":
163
  import json
164
+ import os
165
+
166
+ # Ensure the data directory exists
167
+ os.makedirs("data", exist_ok=True)
168
 
169
  pipeline = AnimeRAGPipeline(
170
+ retriever_k=50,
171
  recommendation_n=5
172
  )
173
 
174
  test_queries = [
175
  "Anime similar to Death Note but lighter in tone",
176
+ "A really obscure and weird sci-fi mecha from the 90s", # Test the hidden gem / passion rate
177
+ "A generic isekai with an overpowered main character" # Test the mainstream math
 
178
  ]
179
 
180
  for query in test_queries:
181
+ print(f"\n" + "="*80)
182
+ print(f"TESTING QUERY: '{query}'")
183
+ print("="*80)
184
 
185
+ # 1. Run the pipeline (This executes the LLM routing, retrieval, reranking, and generation)
186
+ result = pipeline.recommend(user_query=query)
 
 
 
 
 
 
 
 
 
 
 
 
187
 
188
+ # 2. Extract the data for the diagnostic report
189
+ # We need to reach into the pipeline's retriever to see what the raw ChromaDB output was,
190
+ # since the pipeline only returns the *reranked* list.
191
+ # Note: If the LLM chose NOT to search, result["retrieved_count"] will be 0.
192
+
193
+ diagnostic_data = {
194
+ "1_initial_user_query": result["query"],
195
+ "2_llm_routing_decision": "Searched" if result["retrieved_count"] > 0 else "Chatted",
196
+ "3_total_retrieved_from_chroma": result["retrieved_count"],
197
+ "4_total_survived_reranking": result.get("reranked_count", 0),
198
+ "5_final_llm_recommendation_text": result["recommendations"],
199
+
200
+ # The "Before" state: We want to see what ChromaDB found just using embeddings
201
+ "6_raw_chroma_results_before_reranking": [],
202
+
203
+ # The "After" state: We want to see the exact math scores for the top survivors
204
+ "7_reranked_results_with_math": []
205
+ }
206
 
207
+ # If the LLM actually performed a search, let's build the detailed lists
208
+ if result["retrieved_count"] > 0:
209
+
210
+ # We need to get the optimized query that the LLM generated for the tool call.
211
+ # (In a real app, you might want to return `optimized_query` in the `result` dict from `recommend()`)
212
+ # For this test, we will just assume it's the original query if we can't easily grab it here.
213
+
214
+ # Let's format the top 15 Reranked items with all their math exposed
215
+ for rank, anime in enumerate(result["retrieved_animes"]):
216
+ diagnostic_data["7_reranked_results_with_math"].append({
217
+ "rank": rank + 1,
218
+ "title": anime["title"],
219
+ "hybrid_score_final": round(anime.get("final_hybrid_score", 0), 4),
220
+ "semantic_score_raw": round(anime.get("semantic_score", 0), 4),
221
+ "quality_score_raw": round(anime.get("raw_quality_score", 0), 4),
222
+ "bayesian_average": round(anime.get("bayesian_score", 0), 4),
223
+ "passion_rate": round(anime.get("passion_rate", 0), 5),
224
+ "chroma_distance": round(1 - anime.get("relevance_score", 1), 4) # Showing original vector distance
225
+ })
226
+
227
+ # 3. Save the diagnostic report
228
+ safe_filename = f"diagnostic_{query[:20].replace(' ', '_').lower()}.json"
229
+ filepath = os.path.join("data", safe_filename)
230
+
231
+ with open(filepath, "w") as f:
232
+ json.dump(diagnostic_data, f, indent=2)
233
+
234
+ print(f"\n✅ Diagnostic saved to {filepath}")
235
+ print(f"Generated Output:\n{result['recommendations']}")
236
+
237
+ # Pause to respect Groq API rate limits
238
  import time
239
+ time.sleep(3)
src/retrieval/vector_search.py CHANGED
@@ -1,5 +1,14 @@
1
  import chromadb
2
  from sentence_transformers import SentenceTransformer
 
 
 
 
 
 
 
 
 
3
 
4
 
5
  class AnimeRetriever:
@@ -15,12 +24,22 @@ class AnimeRetriever:
15
 
16
  print(f"Loaded collection with {self.collection.count()} anime")
17
 
 
 
 
 
 
 
 
 
 
18
  def search(
19
  self,
20
  query: str,
21
  n_results: int = 5,
22
- genre_filter: str | None = None,
23
- min_score: float | None = None
 
24
  ) -> list[dict]:
25
  """
26
  Search for anime similar to query
@@ -30,50 +49,94 @@ class AnimeRetriever:
30
  n_results: Number of results to return
31
  genre_filter: Optional genre to filter by
32
  min_score: Minimum MAL score (e.g., 7.0)
 
33
 
34
  Returns:
35
  List of dicts with anime info
36
  """
37
- where_clause = {}
 
38
  if min_score:
39
- where_clause["score"] = {"$gte": min_score}
 
 
 
 
 
 
40
 
41
- # Genre Filtering later
 
 
42
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
43
  results = self.collection.query(
44
  query_texts=[query],
45
- n_results=n_results * 2 if genre_filter else n_results,
46
- where=where_clause if where_clause else None
47
  )
 
 
 
 
 
 
48
 
49
  anime_list = []
50
- for i in range(len(results["ids"][0])):
51
- metadata = results["metadatas"][0][i] # type: ignore
52
- distance = results["distances"][0][i] # type: ignore
53
 
54
- # Genre filtering (if specified)
55
- if genre_filter and "genres" in metadata:
56
- # type: ignore
57
- if genre_filter.lower() not in metadata["genres"].lower():
58
- continue
59
 
 
 
 
 
 
 
60
  anime_info = {
61
- "mal_id": results["ids"][0][i],
62
- "title": metadata["title"],
63
- "genres": metadata.get("genres", ""),
64
- "score": metadata["score"],
65
- "type": metadata["type"],
66
- "year": metadata["year"],
67
- "synopsis": metadata["synopsis"],
68
- "distance": distance,
69
- "scored_by": metadata["scored_by"],
70
- "relevance_score": 1 - distance # Convert distance to similarity
 
 
 
 
 
 
 
 
 
 
 
 
 
71
  }
72
  anime_list.append(anime_info)
73
 
74
- if len(anime_list) >= n_results:
75
- break
76
-
77
  return anime_list
78
 
79
  def get_by_title(self, title: str) -> dict | None:
@@ -86,26 +149,42 @@ class AnimeRetriever:
86
  if __name__ == "__main__":
87
  retriever = AnimeRetriever()
88
 
89
- # Test queries
90
- print("=== Test 1: Basic Search ===")
91
- results = retriever.search("dark psychological anime", n_results=5)
92
- for anime in results:
93
- print(
94
- f"- {anime['title']} (score: {anime['score']}, relevance: {anime['relevance_score']:.3f})")
95
-
96
- print("\n=== Test 2: Genre Filter ===")
97
- results = retriever.search(
98
- "high school", n_results=5, genre_filter="Comedy")
99
- for anime in results:
100
- print(f"- {anime['title']} ({anime['genres']})")
101
 
102
- print("\n=== Test 3: Score Filter ===")
103
- results = retriever.search("adventure", n_results=5, min_score=8.0)
104
- for anime in results:
105
- print(f"- {anime['title']} (score: {anime['score']})")
 
106
 
107
- print("\n=== Test 4: Scored by Filter ===")
108
- results = retriever.search("adventure", n_results=5, min_score=8.0)
 
109
  for anime in results:
110
- print(
111
- f"- {anime['title']} (score: {anime['score']}) (scored_by: {anime['scored_by']})")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import chromadb
2
  from sentence_transformers import SentenceTransformer
3
+ from src.database.session import engine
4
+ from src.database.models import Animes
5
+ from sqlmodel import Session, select
6
+ import logging
7
+
8
+
9
+ logger = logging.getLogger(__name__)
10
+ logging.basicConfig(level=logging.INFO,
11
+ format='%(asctime)s - %(levelname)s - %(message)s')
12
 
13
 
14
  class AnimeRetriever:
 
24
 
25
  print(f"Loaded collection with {self.collection.count()} anime")
26
 
27
+ def fetch_anime_batch_from_postgres(self, mal_ids: list[int]) -> dict[int, Animes]:
28
+ """Fetch multiple animes at once and return a dictionary mapped by mal_id"""
29
+ with Session(engine) as session:
30
+ # The .in_() operator acts like SQL's "WHERE mal_id IN (1, 2, 3)"
31
+ statement = select(Animes).where(Animes.mal_id.in_(mal_ids))
32
+ results = session.exec(statement).all()
33
+
34
+ return {anime.mal_id: anime for anime in results}
35
+
36
  def search(
37
  self,
38
  query: str,
39
  n_results: int = 5,
40
+ genre_filter: list[str] | None = None,
41
+ min_score: float | None = 6.0,
42
+ anime_type: str | None = None
43
  ) -> list[dict]:
44
  """
45
  Search for anime similar to query
 
49
  n_results: Number of results to return
50
  genre_filter: Optional genre to filter by
51
  min_score: Minimum MAL score (e.g., 7.0)
52
+ anime_type: Type of Anime (e.g. TV, Movie, etc)
53
 
54
  Returns:
55
  List of dicts with anime info
56
  """
57
+ # 1. Build chromadb WHERE clause for filtering
58
+ conditions = [{"scored_by": {"$gte": 9000}}]
59
  if min_score:
60
+ logger.info(f"SCORE: Filtered based on min_score: {min_score}")
61
+ conditions.append({"score": {"$gte": min_score}})
62
+
63
+ if anime_type:
64
+ logger.info(
65
+ f"ANIME TYPE: Filtered based on anime_type: {anime_type}")
66
+ conditions.append({"type": {"$eq": anime_type}})
67
 
68
+ if genre_filter:
69
+ logger.info(
70
+ f"GENRE: Pre-filtering (OR) for genres: {', '.join(genre_filter)}")
71
 
72
+ genre_or_conditions = [
73
+ {"genres": {"$contains": genre}} for genre in genre_filter]
74
+
75
+ if len(genre_or_conditions) == 1:
76
+ conditions.append(genre_or_conditions[0])
77
+ else:
78
+ conditions.append({"$or": genre_or_conditions})
79
+
80
+ where_clause = None
81
+ if len(conditions) == 1:
82
+ where_clause = conditions[0]
83
+ elif len(conditions) > 1:
84
+ where_clause = {"$and": conditions}
85
+
86
+ # 2. Query ChromaDB
87
  results = self.collection.query(
88
  query_texts=[query],
89
+ n_results=n_results,
90
+ where=where_clause
91
  )
92
+ if not results["ids"][0]:
93
+ return []
94
+
95
+ # 3. Batch fetch from PostgreSQL
96
+ retrieved_ids = [int(id_str) for id_str in results["ids"][0]]
97
+ postgres_data_map = self.fetch_anime_batch_from_postgres(retrieved_ids)
98
 
99
  anime_list = []
100
+ for i, id_str in enumerate(results["ids"][0]):
101
+ mal_id = int(id_str)
102
+ distance = results["distances"][0][i]
103
 
104
+ # Get the rich data from our Postgres map
105
+ pg_anime = postgres_data_map.get(mal_id)
 
 
 
106
 
107
+ if not pg_anime:
108
+ logger.warning(
109
+ f"Anime ID {mal_id} found in Chroma but missing in Postgres!")
110
+ continue
111
+
112
+ # Merge Chroma math with Postgres truths
113
  anime_info = {
114
+ "mal_id": pg_anime.mal_id,
115
+ "mal_url": pg_anime.url,
116
+ "title": pg_anime.title,
117
+ "title_english": pg_anime.title_english,
118
+ "score": pg_anime.score,
119
+ "scored_by": pg_anime.scored_by,
120
+ "type": pg_anime.type,
121
+ "year": pg_anime.year,
122
+ "genres": pg_anime.genres, # Now an actual list again!
123
+ "studios": pg_anime.studios,
124
+ "themes": pg_anime.themes,
125
+ "demographics": pg_anime.demographics,
126
+ "episodes": pg_anime.episodes,
127
+ "popularity": pg_anime.popularity,
128
+ "rating": pg_anime.rating,
129
+ "aired_from": pg_anime.aired_from,
130
+ "aired_to": pg_anime.aired_to,
131
+ "favorites": pg_anime.favorites,
132
+ "images": pg_anime.images,
133
+ "synopsis": pg_anime.synopsis,
134
+ "searchable_text": pg_anime.searchable_text,
135
+ # Clean rounded score
136
+ "relevance_score": round(1 - distance, 3),
137
  }
138
  anime_list.append(anime_info)
139
 
 
 
 
140
  return anime_list
141
 
142
  def get_by_title(self, title: str) -> dict | None:
 
149
  if __name__ == "__main__":
150
  retriever = AnimeRetriever()
151
 
152
+ # # Test queries
153
+ # print("=== Test 1: Basic Search ===")
154
+ # results = retriever.search("dark psychological anime", n_results=15)
155
+ # for anime in results:
156
+ # print(
157
+ # f"- {anime['title']} (score: {anime['score']}, relevance: {anime['relevance_score']:.3f})")
 
 
 
 
 
 
158
 
159
+ # print("\n=== Test 2: Genre Filter ===")
160
+ # results = retriever.search(
161
+ # "high school", n_results=30, genre_filter=["Fantasy", "Action", "Comedy", "Adventure"])
162
+ # for anime in results:
163
+ # print(f"- {anime['title']} ({anime['genres']})")
164
 
165
+ print("\n=== Test 3: Genre Filter ===")
166
+ results = retriever.search(
167
+ "Overpowered Main character", n_results=5, genre_filter=["Adventure"])
168
  for anime in results:
169
+ # print(
170
+ # f"- {anime['title']} ({anime['genres']}) (Score: {anime["score"]}) (Scored by: {anime["scored_by"]})")
171
+ print(anime)
172
+ break
173
+
174
+ # print("\n=== Test 3: Score Filter ===")
175
+ # results = retriever.search("adventure", n_results=5, min_score=9.0)
176
+ # for anime in results:
177
+ # print(f"- {anime['title']} (score: {anime['score']})")
178
+
179
+ # print("\n=== Test 4: Scored by Filter ===")
180
+ # results = retriever.search("adventure", n_results=5, min_score=8.0)
181
+ # for anime in results:
182
+ # print(
183
+ # f"- {anime['title']} (score: {anime['score']}) (scored_by: {anime['scored_by']})")
184
+
185
+ # print("\n=== Test 5: TYPE Filter ===")
186
+ # results = retriever.search(
187
+ # "Attack On Titan", n_results=5, anime_type="Special")
188
+ # for anime in results:
189
+ # print(
190
+ # f"- {anime['title']} (Anime Type: {anime['type']})")
ui/gradio_app.py CHANGED
@@ -2,416 +2,768 @@ import gradio as gr
2
  from gradio import themes
3
  import requests
4
  import os
 
5
 
6
  API_URL = os.getenv("API_URL", "http://127.0.0.1:8000")
7
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8
 
9
- def get_recommendations(query, min_score, genre_filter, n_results):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
10
  if not query or not query.strip():
11
  return (
12
- "<div class='status-badge warning'>Waiting for prompt</div>",
13
- "Tell me what you're in the mood for to get started."
14
  )
15
-
16
  try:
17
- payload = {
18
- "query": query.strip(),
19
- "n_results": int(n_results)
20
- }
21
  if min_score > 0:
22
  payload["min_score"] = float(min_score)
23
- if genre_filter and genre_filter != "Any":
24
  payload["genre_filter"] = genre_filter
 
 
25
 
26
  response = requests.post(
27
  f"{API_URL}/recommend", json=payload, timeout=30)
28
  response.raise_for_status()
29
  result = response.json()
30
 
31
- recommendations_text = result.get("recommendations", "")
32
- retrieved_count = result.get("retrieved_count", 0)
33
- time_taken = result.get("metadata", {}).get(
34
- "Time taken for LLM + vector search", "unknown")
35
-
36
- if not recommendations_text or recommendations_text.strip() == "":
37
  return (
38
- "<div class='status-badge error'>No Matches</div>",
39
- "Couldn't find anything matching that exact vibe. Try tweaking your search."
40
  )
41
-
42
- header_html = f"""
43
- <div class='results-dashboard'>
44
- <div class='metric'>
45
- <span class='label'>TITLES SCANNED</span>
46
- <span class='value'>{retrieved_count}</span>
47
- </div>
48
- <div class='metric'>
49
- <span class='label'>SEARCH TIME</span>
50
- <span class='value'>{time_taken}</span>
51
- </div>
52
- <div class='status-badge success'>Search Complete</div>
53
- </div>
54
- """
55
- return header_html, recommendations_text
56
 
57
  except requests.exceptions.ConnectionError:
58
  return (
59
- "<div class='status-badge error'>Offline</div>",
60
- "**Error:** Cannot connect to backend. Make sure the FastAPI server is running."
61
  )
62
  except Exception as e:
63
- return ("<div class='status-badge error'>Error</div>", f"**Something went wrong:** {str(e)}")
 
 
 
64
 
65
 
 
 
 
66
  CSS = """
67
- @import url('https://fonts.googleapis.com/css2?family=Outfit:wght@400;600;800&family=Inter:wght@400;500&display=swap');
68
 
69
  :root {
70
- --bg-base: #0b0f19;
71
- --bg-panel: #111827;
72
- --accent: #6366f1;
73
- --accent-hover: #4f46e5;
74
- --text-main: #f3f4f6;
75
- --text-muted: #9ca3af;
76
- --border-dim: #1f2937;
 
 
 
 
 
 
 
77
  }
78
 
79
- body, .gradio-container {
80
- background-color: var(--bg-base) !important;
81
- font-family: 'Inter', sans-serif !important;
82
- color: var(--text-main) !important;
 
 
 
83
  }
84
 
85
  .gradio-container {
86
- max-width: 960px !important;
87
- margin: 0 auto !important;
88
- padding: 50px 20px !important;
 
 
 
 
 
 
 
 
 
89
  }
90
 
91
- /* Typography & Headers */
92
- .app-header {
 
 
 
 
 
 
 
93
  text-align: center;
94
- margin-bottom: 40px;
95
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
96
 
97
- .app-title {
98
- font-family: 'Outfit', sans-serif;
99
- font-size: 42px;
100
- font-weight: 800;
101
- color: #ffffff;
102
- margin: 0 0 8px 0;
103
- letter-spacing: -1px;
104
  }
 
105
 
106
- .app-title span { color: var(--accent); }
107
- .app-subtitle { font-size: 16px; color: var(--text-muted); line-height: 1.5; }
 
 
108
 
109
- /* 🔥 NEW Unified Chat Input Bar 🔥 */
110
- #chat-input-container {
111
- background: var(--bg-panel);
112
- border: 1px solid var(--border-dim);
113
- border-radius: 24px;
114
- padding: 8px 8px 8px 16px;
115
- margin-bottom: 16px;
116
- display: flex;
117
- align-items: flex-end;
118
- transition: all 0.2s ease;
119
- box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
120
  }
121
 
122
- #chat-input-container:focus-within {
123
- border-color: var(--accent);
124
- box-shadow: 0 0 0 2px rgba(99, 102, 241, 0.25);
 
 
 
 
 
 
 
 
 
 
 
 
125
  }
126
 
127
- /* Override Gradio's internal Textbox styling */
128
- #query-input {
129
- border: none !important;
130
- box-shadow: none !important;
131
- background: transparent !important;
132
- flex-grow: 1;
 
 
 
 
 
133
  }
134
 
 
 
 
 
 
 
 
 
 
 
135
  #query-input textarea {
 
 
 
136
  background: transparent !important;
 
137
  border: none !important;
138
  box-shadow: none !important;
139
- color: #ffffff !important;
140
- font-family: 'Inter', sans-serif !important;
141
- font-size: 16px !important;
142
- padding: 12px 0 !important;
143
- resize: none !important; /* Removes the little drag handle */
144
  }
145
 
146
- #query-input textarea:focus {
147
- box-shadow: none !important;
 
 
148
  border: none !important;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
149
  }
150
 
151
- /* 🔥 The Circle Send Button 🔥 */
152
- #send-btn {
153
- background: var(--accent) !important;
154
- color: #ffffff !important;
155
- border: none !important;
156
- border-radius: 50% !important;
157
- width: 44px !important;
158
- height: 44px !important;
159
- min-width: 44px !important;
160
- display: flex;
161
- align-items: center;
162
- justify-content: center;
163
- font-size: 20px !important;
164
- padding: 0 !important;
165
- margin-left: 12px;
166
- margin-bottom: 4px; /* Keeps it aligned to the bottom when text expands */
167
- cursor: pointer;
168
- transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1) !important;
 
 
 
 
 
 
 
 
 
 
 
 
169
  }
170
 
171
- #send-btn:hover {
172
- background: var(--accent-hover) !important;
173
- transform: scale(1.05);
174
- box-shadow: 0 4px 12px rgba(99, 102, 241, 0.4) !important;
 
175
  }
176
 
177
- #send-btn:active {
178
- transform: scale(0.95);
 
 
 
 
 
 
 
 
 
 
 
 
179
  }
 
180
 
181
- /* Controls Row */
182
- .control-panel {
183
- background: transparent;
184
- margin-bottom: 30px;
185
  }
186
 
187
- .control-panel .gr-box, .control-panel select {
188
- background: var(--bg-panel) !important;
189
- border: 1px solid var(--border-dim) !important;
190
- border-radius: 8px !important;
 
 
 
 
 
 
 
 
 
 
 
 
191
  }
192
 
193
- label {
194
- font-family: 'Outfit', sans-serif !important;
195
- font-size: 12px !important;
196
- color: var(--text-muted) !important;
197
- text-transform: uppercase;
198
- letter-spacing: 0.5px;
 
 
199
  }
200
 
201
- /* Output Dashboard Elements */
202
- .results-dashboard {
203
- display: flex;
204
- gap: 30px;
205
- align-items: center;
206
- background: var(--bg-panel);
207
- border: 1px solid var(--border-dim);
208
- padding: 16px 24px;
209
- border-radius: 8px;
210
- margin-bottom: 16px;
211
- box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
212
- }
213
-
214
- .metric { display: flex; flex-direction: column; }
215
- .metric .label { font-family: 'Outfit', sans-serif; font-size: 11px; color: var(--text-muted); letter-spacing: 0.5px; }
216
- .metric .value { font-family: 'Outfit', sans-serif; font-size: 16px; font-weight: 600; color: #fff; }
217
-
218
- .status-badge {
219
- margin-left: auto;
220
- font-family: 'Outfit', sans-serif;
221
- font-size: 12px;
222
- padding: 6px 12px;
223
- border-radius: 20px;
224
- font-weight: 600;
225
- }
226
-
227
- .status-badge.success { background: rgba(16, 185, 129, 0.1); color: #10b981; }
228
- .status-badge.error { background: rgba(239, 68, 68, 0.1); color: #ef4444; }
229
- .status-badge.warning { background: rgba(245, 158, 11, 0.1); color: #f59e0b; }
230
-
231
- /* Markdown Styling for LLM Output */
232
- .markdown-output {
233
- background: transparent;
234
- font-size: 16px;
235
- line-height: 1.8;
236
- color: var(--text-main);
237
- }
238
- .markdown-output h1, .markdown-output h2, .markdown-output h3 { font-family: 'Outfit', sans-serif; color: #fff; margin-top: 24px; margin-bottom: 12px; }
239
- .markdown-output strong { color: #a855f7; font-weight: 600; }
240
- .markdown-output a { color: var(--accent); text-decoration: none; }
241
- .markdown-output a:hover { text-decoration: underline; }
242
-
243
- /* Quick Tags */
244
- .quick-tags { display: flex; gap: 10px; flex-wrap: wrap; margin-top: 12px; justify-content: center; }
245
- .quick-tag {
246
- font-size: 13px;
247
- color: var(--text-muted);
248
- background: var(--bg-panel);
249
- border: 1px solid var(--border-dim);
250
- padding: 6px 14px;
251
- border-radius: 20px;
252
- cursor: pointer;
253
- transition: all 0.2s;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
254
  }
255
- .quick-tag:hover {
256
- color: #fff;
257
- border-color: var(--accent);
258
- box-shadow: 0 0 10px rgba(99, 102, 241, 0.2);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
259
  }
260
  """
261
 
262
- JS_INTERACTION = """
 
 
 
263
  <script>
264
- (function() {
265
- // 1. Handle clicking the quick suggestion tags
266
- document.addEventListener('click', (e) => {
267
- const tag = e.target.closest('.quick-tag');
268
- if (tag) {
269
- const text = tag.getAttribute('data-text');
270
- const textarea = document.querySelector('textarea');
271
- if (textarea && text) {
272
- textarea.value = text;
273
- textarea.dispatchEvent(new Event('input', { bubbles: true }));
274
-
275
- // Find and click the send button
276
- let btn = document.querySelector('#send-btn');
277
- if (btn && btn.tagName !== 'BUTTON') btn = btn.querySelector('button') || btn;
278
  if (btn) btn.click();
279
- }
280
  }
281
  });
282
 
283
- // 2. Override Gradio's internal key bindings
284
- document.addEventListener('keydown', (e) => {
285
- // Target our specific textarea
286
- if (e.target.tagName.toLowerCase() === 'textarea') {
287
-
288
- if (e.key === 'Enter') {
289
-
290
- if (e.shiftKey) {
291
- // SHIFT + ENTER: We want a new line.
292
- // Stop Gradio from seeing this and triggering its default "Submit"
293
- e.stopPropagation();
294
- e.stopImmediatePropagation();
295
- // We DO NOT preventDefault, so the browser natively adds the new line.
296
- }
297
- else if (!e.ctrlKey && !e.metaKey) {
298
- // PLAIN ENTER: We want to submit.
299
- // Stop the browser from making a new line
300
- e.preventDefault();
301
- // Stop Gradio from seeing this event at all
302
- e.stopPropagation();
303
- e.stopImmediatePropagation();
304
-
305
- // Click our send button
306
- let btn = document.querySelector('#send-btn');
307
- if (btn && btn.tagName !== 'BUTTON') btn = btn.querySelector('button') || btn;
308
- if (btn) btn.click();
309
- }
310
- }
311
  }
312
- }, true); // Use the capture phase to intercept before React/Gradio does
313
- })();
 
 
 
 
 
 
 
314
  </script>
315
  """
316
 
 
 
 
 
317
 
318
  def create_interface():
319
- with gr.Blocks(title="Anime Recommender") as demo:
 
 
 
320
 
321
- gr.HTML("""
322
- <div class="app-header">
323
- <h1 class="app-title">Find your next <span>anime</span>.</h1>
324
- <p class="app-subtitle">Describe the exact vibe, mood, or story you're looking for.</p>
325
-
326
- <div class="quick-tags">
327
- <div class="quick-tag" data-text="Cozy slice of life set in the countryside">Cozy Countryside</div>
328
- <div class="quick-tag" data-text="Dark fantasy with an unreliable narrator">Dark Fantasy</div>
329
- <div class="quick-tag" data-text="Fast-paced cyberpunk action with great animation">Cyberpunk Action</div>
330
- </div>
331
- </div>
332
- """)
333
-
334
- # 🔥 NEW UNIFIED INPUT BAR 🔥
335
- with gr.Row(elem_id="chat-input-container"):
336
- query_input = gr.Textbox(
337
- label="",
338
- show_label=False,
339
- lines=2,
340
- placeholder="Message the recommender... (e.g., A melancholic sci-fi)",
341
- elem_id="query-input",
342
- scale=10
343
- )
344
 
345
- # Using an upward arrow icon (↑) for the send button
346
- submit_btn = gr.Button(
347
- "",
348
- elem_id="send-btn",
349
- scale=1
350
- )
 
 
351
 
352
- with gr.Group(elem_classes="control-panel"):
353
- with gr.Row():
354
- genre_dropdown = gr.Dropdown(
355
- choices=["Any", "Action", "Sci-Fi", "Slice of Life",
356
- "Fantasy", "Psychological", "Horror", "Romance"],
357
- value="Any",
358
- label="Genre Filter",
359
- )
360
- min_score_slider = gr.Slider(
361
- minimum=0, maximum=10, value=0, step=0.5,
362
- label="Minimum Rating"
363
- )
364
- n_results_slider = gr.Slider(
365
- minimum=1, maximum=8, value=3, step=1,
366
- label="Number of Results"
367
- )
368
 
369
- output_header = gr.HTML(
370
- value="<div class='results-dashboard'><div class='status-badge warning'>Waiting for input</div></div>"
371
- )
372
 
373
- output_text = gr.Markdown(
374
- value="",
375
- elem_classes="markdown-output"
376
- )
377
 
378
- gr.HTML(JS_INTERACTION)
379
 
380
- submit_btn.click(
381
- fn=get_recommendations,
382
- inputs=[query_input, min_score_slider,
383
- genre_dropdown, n_results_slider],
384
- outputs=[output_header, output_text]
385
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
386
 
387
- # Allow native submission as a fallback
388
- query_input.submit(
389
- fn=get_recommendations,
390
- inputs=[query_input, min_score_slider,
391
- genre_dropdown, n_results_slider],
392
- outputs=[output_header, output_text]
393
- )
 
 
 
394
 
395
- return demo
396
 
397
 
398
  if __name__ == "__main__":
399
  demo = create_interface()
400
 
401
  theme = themes.Base(
402
- font=[themes.GoogleFont("Inter"), "ui-sans-serif",
403
- "system-ui", "sans-serif"],
404
  ).set(
405
  body_background_fill="transparent",
406
  block_background_fill="transparent",
407
  block_border_width="0px",
408
  input_background_fill="transparent",
 
 
 
409
  )
410
 
411
  demo.launch(
412
  server_name="0.0.0.0",
413
  server_port=7860,
414
- share=False,
415
  css=CSS,
416
- theme=theme
417
  )
 
2
  from gradio import themes
3
  import requests
4
  import os
5
+ import re
6
 
7
  API_URL = os.getenv("API_URL", "http://127.0.0.1:8000")
8
 
9
+ ANIME_TYPES = ["Any", "TV", "Movie", "OVA", "ONA",
10
+ "Special", "Music", "CM", "PV", "TV Special"]
11
+ GENRES = [
12
+ "Any", "Action", "Adventure", "Avant Garde", "Award Winning", "Boys Love",
13
+ "Comedy", "Drama", "Ecchi", "Erotica", "Fantasy", "Girls Love", "Gourmet",
14
+ "Hentai", "Horror", "Mystery", "Romance", "Sci-Fi", "Slice of Life", "Sports",
15
+ "Supernatural", "Suspense"
16
+ ]
17
+
18
+ SUGGESTIONS = [
19
+ {"label": "🌸 Similar to Naruto",
20
+ "query": "Action packed themed anime similar to Naruto"},
21
+ {"label": "⚔️ Dark fantasy",
22
+ "query": "Dark fantasy with an unreliable narrator"},
23
+ {"label": "🤖 Cyberpunk action",
24
+ "query": "Fast-paced cyberpunk action with great animation"},
25
+ {"label": "💀 Psychological",
26
+ "query": "Psychological thriller that messes with your head"},
27
+ {"label": "💘 Romance drama", "query": "Bittersweet romance that makes you cry"},
28
+ ]
29
+
30
+ # ─── HTML builders ────────────────────────────────────────────────────────────
31
+
32
+
33
+ def _img(images: dict) -> str:
34
+ webp = images.get("webp", {}).get("image_url", "")
35
+ jpg = images.get("jpg", {}).get("image_url", "")
36
+ return webp or jpg or ""
37
+
38
+
39
+ def build_spotlight_card(anime: dict) -> str:
40
+ img = _img(anime.get("images", {}))
41
+ title = anime.get("title", "Unknown")
42
+ eng = anime.get("title_english") or ""
43
+ score = anime.get("score") or "—"
44
+ year = anime.get("year") or "—"
45
+ kind = anime.get("type") or "—"
46
+ eps = anime.get("episodes") or "—"
47
+ genres = anime.get("genres") or []
48
+ studios = anime.get("studios") or []
49
+ url = anime.get("mal_url", "#")
50
+ raw_syn = anime.get("synopsis") or ""
51
+ synopsis = raw_syn[:220].rstrip() + ("..." if len(raw_syn) > 220 else "")
52
+
53
+ genre_pills = "".join(f"<span class='pill'>{g}</span>" for g in genres[:4])
54
+ studio_str = ", ".join(studios[:2]) if studios else "Unknown Studio"
55
+
56
+ eng_html = f"<h4 class='sc-eng'>{eng}</h4>" if eng and eng.strip(
57
+ ) and eng != title else ""
58
 
59
+ try:
60
+ s = float(score)
61
+ sc = "#00e5a0" if s >= 8 else "#fbbf24" if s >= 7 else "#f87171"
62
+ except Exception:
63
+ sc = "#6b7a99"
64
+
65
+ return (
66
+ f"<a href='{url}' target='_blank' class='sc'>"
67
+ f"<div class='sc-poster'>"
68
+ f"<img src='{img}' alt='{title}' loading='lazy' "
69
+ f"onerror=\"this.src='https://placehold.co/280x400/0d1520/00c896?text=?'\"/>"
70
+ f"<div class='sc-badge' style='color:{sc}'>&#9733; {score}</div>"
71
+ f"</div>"
72
+ f"<div class='sc-body'>"
73
+ f"<div class='sc-chips'>"
74
+ f"<span class='mc'>{kind}</span><span class='mc'>{year}</span><span class='mc'>{eps} ep</span>"
75
+ f"</div>"
76
+ f"<h3 class='sc-title'>{title}</h3>"
77
+ f"{eng_html}"
78
+ f"<p class='sc-studio'>{studio_str}</p>"
79
+ f"<p class='sc-synopsis'>{synopsis}</p>"
80
+ f"<div class='pill-row'>{genre_pills}</div>"
81
+ f"<span class='mal-btn'>View on MAL &#8599;</span>"
82
+ f"</div></a>"
83
+ )
84
+
85
+
86
+ def build_retrieved_row(anime: dict, idx: int) -> str:
87
+ img = _img(anime.get("images", {}))
88
+ title = anime.get("title", "Unknown")
89
+ score = anime.get("score") or "—"
90
+ year = anime.get("year") or "—"
91
+ kind = anime.get("type") or ""
92
+ genres = (anime.get("genres") or [])[:2]
93
+ url = anime.get("mal_url", "#")
94
+ raw_score = anime.get("relevance_score", 0)
95
+ normalized_score = (raw_score + 1) / 2
96
+ rel_pct = int(normalized_score * 100)
97
+ genre_str = " · ".join(genres)
98
+
99
+ return (
100
+ f"<a href='{url}' target='_blank' class='rr'>"
101
+ f"<span class='rr-idx'>{idx:02d}</span>"
102
+ f"<img src='{img}' alt='{title}' class='rr-thumb' loading='lazy' "
103
+ f"onerror=\"this.src='https://placehold.co/44x62/0d1520/00c896?text=?'\"/>"
104
+ f"<div class='rr-info'>"
105
+ f"<span class='rr-title'>{title}</span>"
106
+ f"<span class='rr-sub'>{kind} · {year} · {genre_str}</span>"
107
+ f"<div class='rr-bar-bg'><div class='rr-bar-fill' style='width:{rel_pct}%'></div></div>"
108
+ f"</div>"
109
+ f"<div class='rr-right'>"
110
+ f"<span class='rr-score'>&#9733; {score}</span>"
111
+ f"<span class='rr-pct'>{rel_pct}%</span>"
112
+ f"</div>"
113
+ f"</a>"
114
+ )
115
+
116
+
117
+ def format_output(result: dict) -> tuple[str, str]:
118
+ recs_text = result.get("recommendations", "")
119
+ retrieved_count = result.get("retrieved_count", 0)
120
+ time_str = result.get("metadata", {}).get(
121
+ "Time taken for LLM + vector search", "?")
122
+ retrieved = result.get("retrieved_animes", [])
123
+
124
+ header = (
125
+ "<div class='hud'>"
126
+ "<div class='hud-pill ready'>&#10022; Results ready</div>"
127
+ "<div class='hud-stats'>"
128
+ f"<div class='hud-stat'><span class='hud-n'>{retrieved_count}</span>"
129
+ f"<span class='hud-l'>titles scanned</span></div>"
130
+ "<div class='hud-sep'></div>"
131
+ f"<div class='hud-stat'><span class='hud-n'>{time_str[:4]}s</span>"
132
+ f"<span class='hud-l'>search time</span></div>"
133
+ "</div></div>"
134
+ )
135
+
136
+ sections = re.split(r'\*\*(.+?)\*\*', recs_text)
137
+ title_map = {a.get("title", "").lower(): a for a in retrieved}
138
+ title_map.update(
139
+ {(a.get("title_english") or "").lower(): a for a in retrieved})
140
+
141
+ recs_html = ""
142
+ if len(sections) > 1:
143
+ pre = sections[0].strip()
144
+ if pre:
145
+ recs_html += f"<p class='llm-intro'>{pre}</p>"
146
+
147
+ pairs = list(zip(sections[1::2], sections[2::2]))
148
+ for i, (rec_title, rec_body) in enumerate(pairs):
149
+ rec_title = rec_title.strip()
150
+ rec_body = rec_body.strip().lstrip("\n").rstrip("\n—-").strip()
151
+
152
+ matched = title_map.get(rec_title.lower())
153
+ if not matched:
154
+ for k, v in title_map.items():
155
+ if k and (k in rec_title.lower() or rec_title.lower() in k):
156
+ matched = v
157
+ break
158
+
159
+ card = build_spotlight_card(
160
+ matched) if matched else f"<div class='no-img-title'>{rec_title}</div>"
161
+ delay = f"{i * 0.09:.2f}"
162
+ recs_html += (
163
+ f"<div class='rec-block' style='animation-delay:{delay}s'>"
164
+ f"{card}"
165
+ f"<div class='llm-pitch'>{rec_body}</div>"
166
+ f"</div>"
167
+ )
168
+ else:
169
+ recs_html = f"<div class='convo-msg'>{recs_text}</div>"
170
+
171
+ sidebar = ""
172
+ if retrieved:
173
+ rows = "".join(build_retrieved_row(a, i + 1)
174
+ for i, a in enumerate(retrieved))
175
+ sidebar = (
176
+ "<aside class='sidebar'>"
177
+ "<div class='sidebar-head'>"
178
+ "<span class='sidebar-label'>Vector search</span>"
179
+ f"<span class='sidebar-count'>{retrieved_count}</span>"
180
+ "</div>"
181
+ f"<div class='rr-list'>{rows}</div>"
182
+ "</aside>"
183
+ )
184
+
185
+ body = (
186
+ "<div class='main-layout'>"
187
+ "<section class='recs-col'>"
188
+ "<div class='recs-heading'>Recommendations</div>"
189
+ f"{recs_html}"
190
+ "</section>"
191
+ f"{sidebar}"
192
+ "</div>"
193
+ )
194
+ return header, body
195
+
196
+
197
+ def get_recommendations(query, min_score, genre_filter, anime_type, n_results):
198
  if not query or not query.strip():
199
  return (
200
+ "<div class='hud'><div class='hud-pill idle'>Waiting for your prompt...</div></div>",
201
+ ""
202
  )
 
203
  try:
204
+ payload = {"query": query.strip(), "n_results": int(n_results)}
 
 
 
205
  if min_score > 0:
206
  payload["min_score"] = float(min_score)
207
+ if genre_filter and "Any" not in genre_filter:
208
  payload["genre_filter"] = genre_filter
209
+ if anime_type and anime_type != "Any":
210
+ payload["anime_type"] = anime_type
211
 
212
  response = requests.post(
213
  f"{API_URL}/recommend", json=payload, timeout=30)
214
  response.raise_for_status()
215
  result = response.json()
216
 
217
+ if not result.get("recommendations", "").strip():
 
 
 
 
 
218
  return (
219
+ "<div class='hud'><div class='hud-pill err'>No matches</div></div>",
220
+ "<p class='convo-msg'>Couldn't find anything for that vibe. Try different keywords.</p>"
221
  )
222
+ return format_output(result)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
223
 
224
  except requests.exceptions.ConnectionError:
225
  return (
226
+ "<div class='hud'><div class='hud-pill err'>Offline</div></div>",
227
+ "<p class='convo-msg'>Cannot reach the backend. Is FastAPI running?</p>"
228
  )
229
  except Exception as e:
230
+ return (
231
+ "<div class='hud'><div class='hud-pill err'>Error</div></div>",
232
+ f"<p class='convo-msg'>Something went wrong: {e}</p>"
233
+ )
234
 
235
 
236
+ # =============================================================================
237
+ # CSS (SCALED DOWN TO "AVERAGE" SIZE)
238
+ # =============================================================================
239
  CSS = """
240
+ @import url('https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@300;400;500;600;700;800&display=swap');
241
 
242
  :root {
243
+ --bg: #070c12;
244
+ --bg2: #121b29;
245
+ --bg3: #1a2639;
246
+ --bdr: rgba(255,255,255,0.08);
247
+ --acc: #00c896;
248
+ --acc2: #00e8b0;
249
+ --adim: rgba(0,200,150,0.12);
250
+ --aglow: rgba(0,200,150,0.3);
251
+ --amb: #fbbf24;
252
+ --red: #f87171;
253
+ --txt: #dde3f0;
254
+ --mut: #6b8299;
255
+ --f: 'Plus Jakarta Sans', sans-serif;
256
+ --sw: 220px; /* Reduced sidebar width */
257
  }
258
 
259
+ *, *::before, *::after { box-sizing: border-box; }
260
+
261
+ html, body {
262
+ background: var(--bg) !important;
263
+ font-family: var(--f) !important;
264
+ color: var(--txt) !important;
265
+ margin: 0; padding: 0;
266
  }
267
 
268
  .gradio-container {
269
+ max-width: 100% !important;
270
+ padding: 0 !important;
271
+ margin: 0 !important;
272
+ }
273
+
274
+ #main-content {
275
+ margin-left: var(--sw);
276
+ width: calc(100% - var(--sw));
277
+ padding: 0;
278
+ min-height: 100vh;
279
+ display: flex;
280
+ flex-direction: column;
281
  }
282
 
283
+ footer { display: none !important; }
284
+
285
+ /* ── HERO BRAND ──────────────────────────────────────────────── */
286
+ .hero-brand {
287
+ display: flex;
288
+ flex-direction: column;
289
+ align-items: center;
290
+ justify-content: center;
291
+ padding: 40px 20px 20px;
292
  text-align: center;
 
293
  }
294
+ .hero-logo-row {
295
+ display: flex;
296
+ align-items: center;
297
+ gap: 12px;
298
+ margin-bottom: 6px;
299
+ }
300
+ .hero-icon {
301
+ width: 38px; height: 38px;
302
+ background: linear-gradient(135deg, var(--acc), var(--acc2));
303
+ border-radius: 12px;
304
+ display: flex; align-items: center; justify-content: center;
305
+ font-size: 20px; flex-shrink: 0;
306
+ box-shadow: 0 4px 16px var(--aglow);
307
+ color: #070c12;
308
+ }
309
+ .hero-name { font-size: 24px; font-weight: 800; color: #fff; letter-spacing: -.5px; }
310
+ .hero-name em { font-style: normal; color: var(--acc); }
311
+
312
+ /* ── FIXED SIDEBAR ───────────────────────────────────────────── */
313
+ #anifind-sidebar {
314
+ position: fixed;
315
+ top: 0; left: 0;
316
+ width: var(--sw);
317
+ height: 100vh;
318
+ background: var(--bg);
319
+ border-right: 1px solid var(--bdr);
320
+ display: flex;
321
+ flex-direction: column;
322
+ padding: 30px 20px;
323
+ overflow-y: auto;
324
+ z-index: 999;
325
+ scrollbar-width: none;
326
+ box-shadow: 4px 0 20px rgba(0,0,0,0.2);
327
+ }
328
+ #anifind-sidebar::-webkit-scrollbar { display: none; }
329
 
330
+ .sb-tagline {
331
+ font-size: 20px; font-weight: 800; color: #fff;
332
+ line-height: 1.3; letter-spacing: -.3px; margin-bottom: 10px;
 
 
 
 
333
  }
334
+ .sb-tagline em { font-style: normal; color: var(--acc); }
335
 
336
+ .sb-sub {
337
+ font-size: 13px; color: var(--mut);
338
+ line-height: 1.5; margin-bottom: 30px;
339
+ }
340
 
341
+ .sb-sect {
342
+ font-size: 11px; font-weight: 700;
343
+ letter-spacing: 1.2px; text-transform: uppercase;
344
+ color: var(--mut); margin-bottom: 12px;
 
 
 
 
 
 
 
345
  }
346
 
347
+ .chips-col { display: flex; flex-direction: column; gap: 8px; }
348
+
349
+ .chip {
350
+ font-family: var(--f); font-size: 13px; font-weight: 600;
351
+ color: var(--txt); background: var(--bg2);
352
+ border: 1px solid var(--bdr);
353
+ padding: 10px 14px; border-radius: 10px;
354
+ cursor: pointer; text-align: left;
355
+ transition: all .3s cubic-bezier(0.34, 1.56, 0.64, 1);
356
+ }
357
+ .chip:hover {
358
+ color: var(--acc); background: var(--bg3);
359
+ border-color: rgba(0,200,150,.5);
360
+ transform: translateX(4px) scale(1.02);
361
+ box-shadow: 0 4px 12px rgba(0,0,0,0.25);
362
  }
363
 
364
+ /* ── STICKY SEARCH BAR ───────────────────────────────────────── */
365
+ #search-zone {
366
+ position: sticky !important;
367
+ top: 0 !important; z-index: 100;
368
+ background: rgba(7,12,18,0.85) !important;
369
+ backdrop-filter: blur(16px);
370
+ -webkit-backdrop-filter: blur(16px);
371
+ border-bottom: 1px solid var(--bdr) !important;
372
+ padding: 16px 24px !important;
373
+ gap: 12px !important;
374
+ align-items: center !important;
375
  }
376
 
377
+ #query-input {
378
+ background: var(--bg2) !important;
379
+ border: 2px solid var(--bdr) !important;
380
+ border-radius: 12px !important;
381
+ transition: all .3s cubic-bezier(0.16, 1, 0.3, 1) !important;
382
+ }
383
+ #query-input:focus-within {
384
+ border-color: var(--acc) !important;
385
+ box-shadow: 0 0 0 3px var(--adim) !important;
386
+ }
387
  #query-input textarea {
388
+ font-family: var(--f) !important;
389
+ font-size: 13px !important;
390
+ padding: 10px !important;
391
  background: transparent !important;
392
+ color: #fff !important;
393
  border: none !important;
394
  box-shadow: none !important;
 
 
 
 
 
395
  }
396
 
397
+ #send-btn { min-width: unset !important; }
398
+ #send-btn button {
399
+ background: linear-gradient(135deg, var(--acc), #00ffc8) !important;
400
+ color: #000 !important;
401
  border: none !important;
402
+ border-radius: 12px !important;
403
+ height: 42px !important;
404
+ width: 42px !important;
405
+ font-size: 18px !important;
406
+ font-weight: 900 !important;
407
+ cursor: pointer;
408
+ transition: all .3s cubic-bezier(0.34, 1.56, 0.64, 1) !important;
409
+ box-shadow: 0 4px 14px rgba(0, 200, 150, 0.4), inset 0 2px 0 rgba(255,255,255,0.3) !important;
410
+ display: flex; align-items: center; justify-content: center;
411
+ }
412
+ #send-btn button:hover {
413
+ transform: translateY(-2px) scale(1.05) !important;
414
+ box-shadow: 0 8px 20px rgba(0, 200, 150, 0.6) !important;
415
+ }
416
+ #send-btn button:active { transform: scale(0.95) !important; }
417
+
418
+ /* ── FILTER BAR ──────────────────────────────────────────────── */
419
+ #filter-bar {
420
+ background: var(--bg2) !important;
421
+ border: 1px solid var(--bdr) !important;
422
+ border-radius: 14px !important;
423
+ margin: 16px 24px 0 !important;
424
+ padding: 16px 24px !important;
425
+ box-shadow: 0 4px 16px rgba(0,0,0,0.15) !important;
426
+ gap: 16px !important;
427
+ }
428
+ #filter-bar label span {
429
+ color: var(--mut) !important;
430
+ font-weight: 700 !important;
431
+ text-transform: uppercase !important;
432
+ letter-spacing: 1px !important;
433
+ font-size: 11px !important;
434
  }
435
 
436
+ /* ── OUTPUT ZONE ─────────────────────────────────────────────── */
437
+ #output-zone {
438
+ padding: 24px !important;
439
+ flex: 1;
440
+ }
441
+
442
+ .hud {
443
+ display: flex; align-items: center; justify-content: space-between;
444
+ background: var(--bg2); border: 1px solid var(--bdr);
445
+ border-radius: 12px; padding: 12px 20px;
446
+ margin-bottom: 24px;
447
+ box-shadow: 0 2px 8px rgba(0,0,0,0.1);
448
+ }
449
+ .hud-pill {
450
+ font-size: 13px; font-weight: 700;
451
+ padding: 6px 14px; border-radius: 100px;
452
+ }
453
+ .hud-pill.ready { background: var(--adim); color: var(--acc); border: 1px solid rgba(0,200,150,.3); }
454
+ .hud-pill.idle { background: rgba(251,191,36,.1); color: var(--amb); border: 1px solid rgba(251,191,36,.2); }
455
+ .hud-pill.err { background: rgba(248,113,113,.1); color: var(--red); border: 1px solid rgba(248,113,113,.2); }
456
+
457
+ .hud-stats { display: flex; align-items: center; gap: 16px; }
458
+ .hud-stat { display: flex; flex-direction: column; align-items: flex-end; }
459
+ .hud-n { font-size: 18px; font-weight: 800; color: #fff; line-height: 1; }
460
+ .hud-l { font-size: 11px; font-weight: 600; color: var(--mut); text-transform: uppercase; margin-top: 4px; }
461
+ .hud-sep { width: 1px; height: 28px; background: var(--bdr); }
462
+
463
+ .main-layout {
464
+ display: grid; grid-template-columns: 1fr 350px;
465
+ gap: 24px; align-items: start;
466
  }
467
 
468
+ .recs-heading {
469
+ font-size: 12px; font-weight: 800; letter-spacing: 1.5px;
470
+ text-transform: uppercase; color: var(--mut);
471
+ margin-bottom: 18px; padding-bottom: 10px;
472
+ border-bottom: 2px solid var(--bdr);
473
  }
474
 
475
+ /* ── RECOMMENDATION BLOCK ────────────────────────────────────── */
476
+ .rec-block {
477
+ background: var(--bg2);
478
+ border: 1px solid var(--bdr);
479
+ border-radius: 16px; overflow: hidden; margin-bottom: 20px;
480
+ opacity: 0; transform: translateY(15px);
481
+ animation: up .4s cubic-bezier(0.16, 1, 0.3, 1) forwards;
482
+ transition: all .3s cubic-bezier(0.175, 0.885, 0.32, 1.275);
483
+ box-shadow: 0 4px 16px rgba(0,0,0,0.1);
484
+ }
485
+ .rec-block:hover {
486
+ border-color: rgba(0,200,150,.4);
487
+ box-shadow: 0 8px 30px rgba(0,200,150,.15);
488
+ transform: translateY(-4px) scale(1.01);
489
  }
490
+ @keyframes up { to { opacity: 1; transform: translateY(0); } }
491
 
492
+ .sc {
493
+ display: grid; grid-template-columns: 130px 1fr;
494
+ text-decoration: none !important; color: inherit !important;
 
495
  }
496
 
497
+ .sc-poster {
498
+ position: relative; overflow: hidden;
499
+ background: var(--bg3); min-height: 180px;
500
+ }
501
+ .sc-poster img {
502
+ width: 100%; height: 100%; object-fit: cover;
503
+ display: block; transition: transform .5s cubic-bezier(0.16, 1, 0.3, 1);
504
+ }
505
+ .sc:hover .sc-poster img { transform: scale(1.06); }
506
+
507
+ .sc-badge {
508
+ position: absolute; top: 10px; left: 10px;
509
+ background: rgba(7,12,18,.85); backdrop-filter: blur(8px);
510
+ font-size: 12px; font-weight: 800; padding: 4px 10px;
511
+ border-radius: 100px; border: 1px solid rgba(255,255,255,.15);
512
+ box-shadow: 0 2px 8px rgba(0,0,0,0.3);
513
  }
514
 
515
+ .sc-body { padding: 16px; display: flex; flex-direction: column; gap: 8px; }
516
+
517
+ .sc-chips { display: flex; gap: 6px; flex-wrap: wrap; margin-bottom: 2px; }
518
+ .mc {
519
+ font-size: 10px; font-weight: 700; letter-spacing: .5px;
520
+ text-transform: uppercase; color: var(--acc);
521
+ background: var(--adim); border: 1px solid rgba(0,200,150,.25);
522
+ padding: 3px 8px; border-radius: 6px;
523
  }
524
 
525
+ .sc-title { font-size: 17px; font-weight: 800; color: #fff; line-height: 1.2; letter-spacing: -.3px; }
526
+ .sc-eng { font-size: 12px; color: #a0b2c6; font-weight: 600; font-style: italic; margin-top: -2px; }
527
+ .sc-studio { font-size: 12px; color: #8a9bb0; font-weight: 600; }
528
+ .sc-synopsis { font-size: 12.5px; color: #dde3f0; line-height: 1.6; flex: 1; margin-top: 6px; }
529
+
530
+ .pill-row { display: flex; gap: 6px; flex-wrap: wrap; margin-top: 12px; }
531
+ .pill {
532
+ font-size: 12px; font-weight: 600; color: var(--txt);
533
+ background: var(--bg3); border: 1px solid var(--bdr);
534
+ padding: 4px 12px; border-radius: 100px;
535
+ }
536
+
537
+ .mal-btn {
538
+ display: inline-block; margin-top: 14px;
539
+ font-size: 12px; font-weight: 700; color: var(--acc);
540
+ background: var(--adim); border: 1px solid rgba(0,200,150,.3);
541
+ padding: 8px 16px; border-radius: 8px; letter-spacing: .5px;
542
+ text-transform: uppercase; align-self: flex-start;
543
+ transition: all .2s ease;
544
+ }
545
+ .sc:hover .mal-btn {
546
+ background: var(--acc); color: #000;
547
+ transform: translateY(-2px);
548
+ box-shadow: 0 4px 12px rgba(0,200,150,0.3);
549
+ }
550
+
551
+ .llm-pitch {
552
+ padding: 14px 16px; font-size: 14px; color: #fff;
553
+ line-height: 1.6; border-top: 1px solid var(--bdr);
554
+ background: rgba(255,255,255,0.02);
555
+ }
556
+
557
+ /* ── RETRIEVED SIDEBAR ───────────────────────────────────────── */
558
+ .sidebar {
559
+ background: var(--bg2); border: 1px solid var(--bdr);
560
+ border-radius: 16px; padding: 18px;
561
+ position: sticky; top: 110px;
562
+ max-height: calc(100vh - 130px);
563
+ overflow-y: auto; scrollbar-width: thin;
564
+ scrollbar-color: var(--mut) transparent;
565
+ box-shadow: 0 4px 16px rgba(0,0,0,0.1);
566
+ }
567
+
568
+ .sidebar-head {
569
+ display: flex; justify-content: space-between; align-items: center;
570
+ margin-bottom: 16px; padding-bottom: 12px; border-bottom: 1px solid var(--bdr);
571
+ }
572
+ .sidebar-label {
573
+ font-size: 11px; font-weight: 700; letter-spacing: 1px;
574
+ text-transform: uppercase; color: var(--mut);
575
+ }
576
+ .sidebar-count {
577
+ font-size: 12px; font-weight: 700; color: var(--acc);
578
+ background: var(--adim); padding: 3px 10px; border-radius: 100px;
579
+ }
580
+
581
+ .rr-list { display: flex; flex-direction: column; gap: 4px; }
582
+ .rr {
583
+ display: grid; grid-template-columns: 22px 44px 1fr auto;
584
+ gap: 12px; align-items: center; padding: 8px;
585
+ border-radius: 10px; text-decoration: none !important;
586
+ color: inherit !important; transition: all .2s;
587
+ }
588
+ .rr:hover {
589
+ background: var(--bg3);
590
+ transform: translateX(4px);
591
+ }
592
+
593
+ .rr-idx { font-size: 11px; font-weight: 700; color: var(--mut); text-align: center; }
594
+ .rr-thumb {
595
+ width: 44px; height: 62px; object-fit: cover;
596
+ border-radius: 6px; background: var(--bg3); display: block;
597
+ }
598
+ .rr-info { display: flex; flex-direction: column; gap: 4px; overflow: hidden; }
599
+ .rr-title {
600
+ font-size: 13px; font-weight: 700; color: #fff;
601
+ white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
602
+ }
603
+ .rr-sub {
604
+ font-size: 11px; font-weight: 500; color: #8a9bb0;
605
+ white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
606
  }
607
+ .rr-bar-bg { height: 4px; background: var(--bg3); border-radius: 4px; overflow: hidden; margin-top: 2px; }
608
+ .rr-bar-fill {
609
+ height: 100%;
610
+ background: linear-gradient(90deg, var(--acc), var(--acc2));
611
+ border-radius: 4px;
612
+ }
613
+ .rr-right { display: flex; flex-direction: column; align-items: flex-end; gap: 4px; }
614
+ .rr-score { font-size: 12px; font-weight: 700; color: var(--amb); }
615
+ .rr-pct { font-size: 11px; font-weight: 600; color: var(--mut); }
616
+
617
+ /* ── RESPONSIVE DESIGN ───────────────────────────────────────── */
618
+ @media (max-width: 1024px) {
619
+ .main-layout { grid-template-columns: 1fr; }
620
+ .sidebar { position: relative; top: 0; max-height: none; margin-top: 24px; }
621
+ }
622
+ @media (max-width: 860px) {
623
+ #anifind-sidebar { width: 100%; position: relative; height: auto; border-right: none; border-bottom: 1px solid var(--bdr); }
624
+ #main-content { margin-left: 0; width: 100%; }
625
+ .sc { grid-template-columns: 1fr; }
626
+ .sc-poster { min-height: 200px; }
627
  }
628
  """
629
 
630
+ # =============================================================================
631
+ # JAVASCRIPT (INJECTED VIA HEAD TO GUARANTEE EXECUTION)
632
+ # =============================================================================
633
+ JS = """
634
  <script>
635
+ function initAniFindJS() {
636
+ document.body.addEventListener('click', function(e) {
637
+ var chip = e.target.closest('.chip');
638
+ if (!chip) return;
639
+ var q = chip.getAttribute('data-q');
640
+ var ta = document.querySelector('#query-input textarea');
641
+ if (ta && q) {
642
+ // Svelte/React native value setter trigger
643
+ var setter = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, 'value').set;
644
+ setter.call(ta, q);
645
+ ta.dispatchEvent(new Event('input', { bubbles: true }));
646
+
647
+ setTimeout(function() {
648
+ var btn = document.querySelector('#send-btn button');
649
  if (btn) btn.click();
650
+ }, 100);
651
  }
652
  });
653
 
654
+ document.body.addEventListener('keydown', function(e) {
655
+ if (e.target.tagName.toLowerCase() !== 'textarea') return;
656
+ if (e.key === 'Enter' && !e.shiftKey && !e.ctrlKey && !e.metaKey) {
657
+ e.preventDefault();
658
+ e.stopPropagation();
659
+ var btn = document.querySelector('#send-btn button');
660
+ if (btn) btn.click();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
661
  }
662
+ }, true);
663
+ }
664
+
665
+ // Ensure the script runs after DOM is ready
666
+ if (document.readyState === "loading") {
667
+ document.addEventListener("DOMContentLoaded", initAniFindJS);
668
+ } else {
669
+ initAniFindJS();
670
+ }
671
  </script>
672
  """
673
 
674
+ # =============================================================================
675
+ # GRADIO INTERFACE
676
+ # =============================================================================
677
+
678
 
679
  def create_interface():
680
+ chips_html = "".join(
681
+ f"<button class='chip' data-q='{s['query']}'>{s['label']}</button>"
682
+ for s in SUGGESTIONS
683
+ )
684
 
685
+ sidebar_html = (
686
+ "<div id='anifind-sidebar'>"
687
+ "<p class='sb-tagline'>Find your next<br><em>favourite anime.</em></p>"
688
+ "<p class='sb-sub'>Describe a vibe, a mood, a feeling — we'll match it to the perfect title.</p>"
689
+ "<div class='sb-sect'>Quick searches</div>"
690
+ f"<div class='chips-col'>{chips_html}</div>"
691
+ "</div>"
692
+ )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
693
 
694
+ hero_html = (
695
+ "<div class='hero-brand'>"
696
+ "<div class='hero-logo-row'>"
697
+ "<div class='hero-icon'>&#x26E9;</div>"
698
+ "<span class='hero-name'>Ani<em>Find</em></span>"
699
+ "</div>"
700
+ "</div>"
701
+ )
702
 
703
+ # Added `head=JS` to properly inject the event listeners globally
704
+ with gr.Blocks(title="AniFind", head=JS) as demo:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
705
 
706
+ gr.HTML(sidebar_html)
 
 
707
 
708
+ with gr.Column(elem_id="main-content"):
 
 
 
709
 
710
+ gr.HTML(hero_html)
711
 
712
+ with gr.Row(elem_id="search-zone"):
713
+ query_input = gr.Textbox(
714
+ label="", show_label=False, lines=2,
715
+ placeholder="e.g. A melancholic sci-fi about memory and loss...",
716
+ elem_id="query-input", scale=10,
717
+ )
718
+ submit_btn = gr.Button("➔", elem_id="send-btn", scale=1)
719
+
720
+ with gr.Row(elem_id="filter-bar"):
721
+ anime_type_dd = gr.Dropdown(
722
+ choices=ANIME_TYPES, value="Any", label="Type", scale=1)
723
+ genre_dd = gr.Dropdown(
724
+ choices=GENRES, value=["Any"], label="Genre",
725
+ multiselect=True, scale=2)
726
+ score_slider = gr.Slider(
727
+ minimum=0, maximum=10, value=0, step=0.5,
728
+ label="Min Rating", scale=1)
729
+ n_slider = gr.Slider(
730
+ minimum=1, maximum=8, value=3, step=1,
731
+ label="Results", scale=1)
732
 
733
+ with gr.Column(elem_id="output-zone"):
734
+ output_header = gr.HTML(
735
+ value="<div class='hud'><div class='hud-pill idle'>Waiting for your prompt...</div></div>"
736
+ )
737
+ output_body = gr.HTML(value="")
738
+
739
+ ins = [query_input, score_slider, genre_dd, anime_type_dd, n_slider]
740
+ outs = [output_header, output_body]
741
+ submit_btn.click(fn=get_recommendations, inputs=ins, outputs=outs)
742
+ query_input.submit(fn=get_recommendations, inputs=ins, outputs=outs)
743
 
744
+ return demo
745
 
746
 
747
  if __name__ == "__main__":
748
  demo = create_interface()
749
 
750
  theme = themes.Base(
751
+ font=[themes.GoogleFont("Plus Jakarta Sans"),
752
+ "ui-sans-serif", "sans-serif"],
753
  ).set(
754
  body_background_fill="transparent",
755
  block_background_fill="transparent",
756
  block_border_width="0px",
757
  input_background_fill="transparent",
758
+ border_color_primary="transparent",
759
+ shadow_drop="none",
760
+ shadow_drop_lg="none",
761
  )
762
 
763
  demo.launch(
764
  server_name="0.0.0.0",
765
  server_port=7860,
766
+ share=True,
767
  css=CSS,
768
+ theme=theme,
769
  )
uv.lock CHANGED
@@ -54,7 +54,7 @@ dev = [
54
 
55
  [package.metadata]
56
  requires-dist = [
57
- { name = "chromadb", specifier = ">=0.5.0" },
58
  { name = "fastapi", extras = ["all"], specifier = ">=0.122.0" },
59
  { name = "gradio", specifier = ">=6.2.0" },
60
  { name = "langchain", specifier = ">=1.1.3" },
@@ -375,25 +375,16 @@ wheels = [
375
 
376
  [[package]]
377
  name = "build"
378
- version = "1.3.0"
379
  source = { registry = "https://pypi.org/simple" }
380
  dependencies = [
381
  { name = "colorama", marker = "(os_name == 'nt' and platform_machine != 'aarch64' and sys_platform == 'linux') or (os_name == 'nt' and platform_python_implementation != 'CPython' and sys_platform == 'linux') or (os_name == 'nt' and sys_platform != 'darwin' and sys_platform != 'linux')" },
382
  { name = "packaging" },
383
  { name = "pyproject-hooks" },
384
  ]
385
- sdist = { url = "https://files.pythonhosted.org/packages/25/1c/23e33405a7c9eac261dff640926b8b5adaed6a6eb3e1767d441ed611d0c0/build-1.3.0.tar.gz", hash = "sha256:698edd0ea270bde950f53aed21f3a0135672206f3911e0176261a31e0e07b397", size = 48544, upload-time = "2025-08-01T21:27:09.268Z" }
386
- wheels = [
387
- { url = "https://files.pythonhosted.org/packages/cb/8c/2b30c12155ad8de0cf641d76a8b396a16d2c36bc6d50b621a62b7c4567c1/build-1.3.0-py3-none-any.whl", hash = "sha256:7145f0b5061ba90a1500d60bd1b13ca0a8a4cebdd0cc16ed8adf1c0e739f43b4", size = 23382, upload-time = "2025-08-01T21:27:07.844Z" },
388
- ]
389
-
390
- [[package]]
391
- name = "cachetools"
392
- version = "6.2.4"
393
- source = { registry = "https://pypi.org/simple" }
394
- sdist = { url = "https://files.pythonhosted.org/packages/bc/1d/ede8680603f6016887c062a2cf4fc8fdba905866a3ab8831aa8aa651320c/cachetools-6.2.4.tar.gz", hash = "sha256:82c5c05585e70b6ba2d3ae09ea60b79548872185d2f24ae1f2709d37299fd607", size = 31731, upload-time = "2025-12-15T18:24:53.744Z" }
395
  wheels = [
396
- { url = "https://files.pythonhosted.org/packages/2c/fc/1d7b80d0eb7b714984ce40efc78859c022cd930e402f599d8ca9e39c78a4/cachetools-6.2.4-py3-none-any.whl", hash = "sha256:69a7a52634fed8b8bf6e24a050fb60bff1c9bd8f6d24572b99c32d4e71e62a51", size = 11551, upload-time = "2025-12-15T18:24:52.332Z" },
397
  ]
398
 
399
  [[package]]
@@ -512,7 +503,7 @@ wheels = [
512
 
513
  [[package]]
514
  name = "chromadb"
515
- version = "1.3.7"
516
  source = { registry = "https://pypi.org/simple" }
517
  dependencies = [
518
  { name = "bcrypt" },
@@ -543,13 +534,13 @@ dependencies = [
543
  { name = "typing-extensions" },
544
  { name = "uvicorn", extra = ["standard"] },
545
  ]
546
- sdist = { url = "https://files.pythonhosted.org/packages/a9/b9/23eb242c0bad56bcac57d9f45a6cc85e016a44ae9baf763c0d040e45e2d7/chromadb-1.3.7.tar.gz", hash = "sha256:393b866b6ac60c12fc0f2a43d07b2884f2d02a68a1b2cb43c5ef87d141543571", size = 1960950, upload-time = "2025-12-12T21:03:13.941Z" }
547
  wheels = [
548
- { url = "https://files.pythonhosted.org/packages/b6/9d/306e220cfb4382e9f29e645339826d1deec64c34cf905c344d0d7345dbdb/chromadb-1.3.7-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:74839c349a740b8e349fabc569f8f4becae9806fa8ff9ca186797bef1f54ee4c", size = 20816599, upload-time = "2025-12-12T21:03:11.173Z" },
549
- { url = "https://files.pythonhosted.org/packages/51/3e/0fbb4c6e7971019c976cf3dbef1c22c1a3089f74ef86c88e2e066edc47e4/chromadb-1.3.7-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:fe9c96f73450274d9f722572afc9d455b4f6f4cd960fa49e4bf489075ef30e6f", size = 20113076, upload-time = "2025-12-12T21:03:07.873Z" },
550
- { url = "https://files.pythonhosted.org/packages/69/78/2ae4064c9b194271b9c2bc66a26a7e11363d13ed2bd691a563fac1a7c5f2/chromadb-1.3.7-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:972cb168033db76a4bb1031bc38b6cc4e6d05ef716c1ffce8ae95a1a3b515dd2", size = 20738619, upload-time = "2025-12-12T21:03:01.409Z" },
551
- { url = "https://files.pythonhosted.org/packages/01/5d/3aa34cb02c3c0e4920a47da5d9092cab690fcbf6df13ec744eacf96891d6/chromadb-1.3.7-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3e05190236e309b54165866dd11676c2702a35b73beaa29502741f22f333c51a", size = 21654395, upload-time = "2025-12-12T21:03:04.909Z" },
552
- { url = "https://files.pythonhosted.org/packages/00/36/7d2d7b6bb26e53214492d71ccb4e128fa2de4d98a215befb7787deaf2701/chromadb-1.3.7-cp39-abi3-win_amd64.whl", hash = "sha256:4618ba7bb5ef5dbf0d4fd9ce708b912d8cd1ab24d3c81e0e092841f325b2c94d", size = 21874973, upload-time = "2025-12-12T21:03:16.918Z" },
553
  ]
554
 
555
  [[package]]
@@ -573,18 +564,6 @@ wheels = [
573
  { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
574
  ]
575
 
576
- [[package]]
577
- name = "coloredlogs"
578
- version = "15.0.1"
579
- source = { registry = "https://pypi.org/simple" }
580
- dependencies = [
581
- { name = "humanfriendly" },
582
- ]
583
- sdist = { url = "https://files.pythonhosted.org/packages/cc/c7/eed8f27100517e8c0e6b923d5f0845d0cb99763da6fdee00478f91db7325/coloredlogs-15.0.1.tar.gz", hash = "sha256:7c991aa71a4577af2f82600d8f8f3a89f936baeaf9b50a9c197da014e5bf16b0", size = 278520, upload-time = "2021-06-11T10:22:45.202Z" }
584
- wheels = [
585
- { url = "https://files.pythonhosted.org/packages/a7/06/3d6badcf13db419e25b07041d9c7b4a2c331d3f4e7134445ec5df57714cd/coloredlogs-15.0.1-py2.py3-none-any.whl", hash = "sha256:612ee75c546f53e92e70049c9dbfcc18c935a2b9a53b66085ce9ef6a6e5c0934", size = 46018, upload-time = "2021-06-11T10:22:42.561Z" },
586
- ]
587
-
588
  [[package]]
589
  name = "comm"
590
  version = "0.2.3"
@@ -846,11 +825,10 @@ wheels = [
846
 
847
  [[package]]
848
  name = "flatbuffers"
849
- version = "25.9.23"
850
  source = { registry = "https://pypi.org/simple" }
851
- sdist = { url = "https://files.pythonhosted.org/packages/9d/1f/3ee70b0a55137442038f2a33469cc5fddd7e0ad2abf83d7497c18a2b6923/flatbuffers-25.9.23.tar.gz", hash = "sha256:676f9fa62750bb50cf531b42a0a2a118ad8f7f797a511eda12881c016f093b12", size = 22067, upload-time = "2025-09-24T05:25:30.106Z" }
852
  wheels = [
853
- { url = "https://files.pythonhosted.org/packages/ee/1b/00a78aa2e8fbd63f9af08c9c19e6deb3d5d66b4dda677a0f61654680ee89/flatbuffers-25.9.23-py2.py3-none-any.whl", hash = "sha256:255538574d6cb6d0a79a17ec8bc0d30985913b87513a01cce8bcdb6b4c44d0e2", size = 30869, upload-time = "2025-09-24T05:25:28.912Z" },
854
  ]
855
 
856
  [[package]]
@@ -871,20 +849,6 @@ wheels = [
871
  { url = "https://files.pythonhosted.org/packages/51/c7/b64cae5dba3a1b138d7123ec36bb5ccd39d39939f18454407e5468f4763f/fsspec-2025.12.0-py3-none-any.whl", hash = "sha256:8bf1fe301b7d8acfa6e8571e3b1c3d158f909666642431cc78a1b7b4dbc5ec5b", size = 201422, upload-time = "2025-12-03T15:23:41.434Z" },
872
  ]
873
 
874
- [[package]]
875
- name = "google-auth"
876
- version = "2.45.0"
877
- source = { registry = "https://pypi.org/simple" }
878
- dependencies = [
879
- { name = "cachetools" },
880
- { name = "pyasn1-modules" },
881
- { name = "rsa" },
882
- ]
883
- sdist = { url = "https://files.pythonhosted.org/packages/e5/00/3c794502a8b892c404b2dea5b3650eb21bfc7069612fbfd15c7f17c1cb0d/google_auth-2.45.0.tar.gz", hash = "sha256:90d3f41b6b72ea72dd9811e765699ee491ab24139f34ebf1ca2b9cc0c38708f3", size = 320708, upload-time = "2025-12-15T22:58:42.889Z" }
884
- wheels = [
885
- { url = "https://files.pythonhosted.org/packages/c6/97/451d55e05487a5cd6279a01a7e34921858b16f7dc8aa38a2c684743cd2b3/google_auth-2.45.0-py2.py3-none-any.whl", hash = "sha256:82344e86dc00410ef5382d99be677c6043d72e502b625aa4f4afa0bdacca0f36", size = 233312, upload-time = "2025-12-15T22:58:40.777Z" },
886
- ]
887
-
888
  [[package]]
889
  name = "googleapis-common-protos"
890
  version = "1.72.0"
@@ -1012,43 +976,43 @@ wheels = [
1012
 
1013
  [[package]]
1014
  name = "grpcio"
1015
- version = "1.76.0"
1016
  source = { registry = "https://pypi.org/simple" }
1017
  dependencies = [
1018
  { name = "typing-extensions" },
1019
  ]
1020
- sdist = { url = "https://files.pythonhosted.org/packages/b6/e0/318c1ce3ae5a17894d5791e87aea147587c9e702f24122cc7a5c8bbaeeb1/grpcio-1.76.0.tar.gz", hash = "sha256:7be78388d6da1a25c0d5ec506523db58b18be22d9c37d8d3a32c08be4987bd73", size = 12785182, upload-time = "2025-10-21T16:23:12.106Z" }
1021
- wheels = [
1022
- { url = "https://files.pythonhosted.org/packages/a0/00/8163a1beeb6971f66b4bbe6ac9457b97948beba8dd2fc8e1281dce7f79ec/grpcio-1.76.0-cp311-cp311-linux_armv7l.whl", hash = "sha256:2e1743fbd7f5fa713a1b0a8ac8ebabf0ec980b5d8809ec358d488e273b9cf02a", size = 5843567, upload-time = "2025-10-21T16:20:52.829Z" },
1023
- { url = "https://files.pythonhosted.org/packages/10/c1/934202f5cf335e6d852530ce14ddb0fef21be612ba9ecbbcbd4d748ca32d/grpcio-1.76.0-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:a8c2cf1209497cf659a667d7dea88985e834c24b7c3b605e6254cbb5076d985c", size = 11848017, upload-time = "2025-10-21T16:20:56.705Z" },
1024
- { url = "https://files.pythonhosted.org/packages/11/0b/8dec16b1863d74af6eb3543928600ec2195af49ca58b16334972f6775663/grpcio-1.76.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:08caea849a9d3c71a542827d6df9d5a69067b0a1efbea8a855633ff5d9571465", size = 6412027, upload-time = "2025-10-21T16:20:59.3Z" },
1025
- { url = "https://files.pythonhosted.org/packages/d7/64/7b9e6e7ab910bea9d46f2c090380bab274a0b91fb0a2fe9b0cd399fffa12/grpcio-1.76.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:f0e34c2079d47ae9f6188211db9e777c619a21d4faba6977774e8fa43b085e48", size = 7075913, upload-time = "2025-10-21T16:21:01.645Z" },
1026
- { url = "https://files.pythonhosted.org/packages/68/86/093c46e9546073cefa789bd76d44c5cb2abc824ca62af0c18be590ff13ba/grpcio-1.76.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8843114c0cfce61b40ad48df65abcfc00d4dba82eae8718fab5352390848c5da", size = 6615417, upload-time = "2025-10-21T16:21:03.844Z" },
1027
- { url = "https://files.pythonhosted.org/packages/f7/b6/5709a3a68500a9c03da6fb71740dcdd5ef245e39266461a03f31a57036d8/grpcio-1.76.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8eddfb4d203a237da6f3cc8a540dad0517d274b5a1e9e636fd8d2c79b5c1d397", size = 7199683, upload-time = "2025-10-21T16:21:06.195Z" },
1028
- { url = "https://files.pythonhosted.org/packages/91/d3/4b1f2bf16ed52ce0b508161df3a2d186e4935379a159a834cb4a7d687429/grpcio-1.76.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:32483fe2aab2c3794101c2a159070584e5db11d0aa091b2c0ea9c4fc43d0d749", size = 8163109, upload-time = "2025-10-21T16:21:08.498Z" },
1029
- { url = "https://files.pythonhosted.org/packages/5c/61/d9043f95f5f4cf085ac5dd6137b469d41befb04bd80280952ffa2a4c3f12/grpcio-1.76.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:dcfe41187da8992c5f40aa8c5ec086fa3672834d2be57a32384c08d5a05b4c00", size = 7626676, upload-time = "2025-10-21T16:21:10.693Z" },
1030
- { url = "https://files.pythonhosted.org/packages/36/95/fd9a5152ca02d8881e4dd419cdd790e11805979f499a2e5b96488b85cf27/grpcio-1.76.0-cp311-cp311-win32.whl", hash = "sha256:2107b0c024d1b35f4083f11245c0e23846ae64d02f40b2b226684840260ed054", size = 3997688, upload-time = "2025-10-21T16:21:12.746Z" },
1031
- { url = "https://files.pythonhosted.org/packages/60/9c/5c359c8d4c9176cfa3c61ecd4efe5affe1f38d9bae81e81ac7186b4c9cc8/grpcio-1.76.0-cp311-cp311-win_amd64.whl", hash = "sha256:522175aba7af9113c48ec10cc471b9b9bd4f6ceb36aeb4544a8e2c80ed9d252d", size = 4709315, upload-time = "2025-10-21T16:21:15.26Z" },
1032
- { url = "https://files.pythonhosted.org/packages/bf/05/8e29121994b8d959ffa0afd28996d452f291b48cfc0875619de0bde2c50c/grpcio-1.76.0-cp312-cp312-linux_armv7l.whl", hash = "sha256:81fd9652b37b36f16138611c7e884eb82e0cec137c40d3ef7c3f9b3ed00f6ed8", size = 5799718, upload-time = "2025-10-21T16:21:17.939Z" },
1033
- { url = "https://files.pythonhosted.org/packages/d9/75/11d0e66b3cdf998c996489581bdad8900db79ebd83513e45c19548f1cba4/grpcio-1.76.0-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:04bbe1bfe3a68bbfd4e52402ab7d4eb59d72d02647ae2042204326cf4bbad280", size = 11825627, upload-time = "2025-10-21T16:21:20.466Z" },
1034
- { url = "https://files.pythonhosted.org/packages/28/50/2f0aa0498bc188048f5d9504dcc5c2c24f2eb1a9337cd0fa09a61a2e75f0/grpcio-1.76.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d388087771c837cdb6515539f43b9d4bf0b0f23593a24054ac16f7a960be16f4", size = 6359167, upload-time = "2025-10-21T16:21:23.122Z" },
1035
- { url = "https://files.pythonhosted.org/packages/66/e5/bbf0bb97d29ede1d59d6588af40018cfc345b17ce979b7b45424628dc8bb/grpcio-1.76.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:9f8f757bebaaea112c00dba718fc0d3260052ce714e25804a03f93f5d1c6cc11", size = 7044267, upload-time = "2025-10-21T16:21:25.995Z" },
1036
- { url = "https://files.pythonhosted.org/packages/f5/86/f6ec2164f743d9609691115ae8ece098c76b894ebe4f7c94a655c6b03e98/grpcio-1.76.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:980a846182ce88c4f2f7e2c22c56aefd515daeb36149d1c897f83cf57999e0b6", size = 6573963, upload-time = "2025-10-21T16:21:28.631Z" },
1037
- { url = "https://files.pythonhosted.org/packages/60/bc/8d9d0d8505feccfdf38a766d262c71e73639c165b311c9457208b56d92ae/grpcio-1.76.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f92f88e6c033db65a5ae3d97905c8fea9c725b63e28d5a75cb73b49bda5024d8", size = 7164484, upload-time = "2025-10-21T16:21:30.837Z" },
1038
- { url = "https://files.pythonhosted.org/packages/67/e6/5d6c2fc10b95edf6df9b8f19cf10a34263b7fd48493936fffd5085521292/grpcio-1.76.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:4baf3cbe2f0be3289eb68ac8ae771156971848bb8aaff60bad42005539431980", size = 8127777, upload-time = "2025-10-21T16:21:33.577Z" },
1039
- { url = "https://files.pythonhosted.org/packages/3f/c8/dce8ff21c86abe025efe304d9e31fdb0deaaa3b502b6a78141080f206da0/grpcio-1.76.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:615ba64c208aaceb5ec83bfdce7728b80bfeb8be97562944836a7a0a9647d882", size = 7594014, upload-time = "2025-10-21T16:21:41.882Z" },
1040
- { url = "https://files.pythonhosted.org/packages/e0/42/ad28191ebf983a5d0ecef90bab66baa5a6b18f2bfdef9d0a63b1973d9f75/grpcio-1.76.0-cp312-cp312-win32.whl", hash = "sha256:45d59a649a82df5718fd9527ce775fd66d1af35e6d31abdcdc906a49c6822958", size = 3984750, upload-time = "2025-10-21T16:21:44.006Z" },
1041
- { url = "https://files.pythonhosted.org/packages/9e/00/7bd478cbb851c04a48baccaa49b75abaa8e4122f7d86da797500cccdd771/grpcio-1.76.0-cp312-cp312-win_amd64.whl", hash = "sha256:c088e7a90b6017307f423efbb9d1ba97a22aa2170876223f9709e9d1de0b5347", size = 4704003, upload-time = "2025-10-21T16:21:46.244Z" },
1042
- { url = "https://files.pythonhosted.org/packages/fc/ed/71467ab770effc9e8cef5f2e7388beb2be26ed642d567697bb103a790c72/grpcio-1.76.0-cp313-cp313-linux_armv7l.whl", hash = "sha256:26ef06c73eb53267c2b319f43e6634c7556ea37672029241a056629af27c10e2", size = 5807716, upload-time = "2025-10-21T16:21:48.475Z" },
1043
- { url = "https://files.pythonhosted.org/packages/2c/85/c6ed56f9817fab03fa8a111ca91469941fb514e3e3ce6d793cb8f1e1347b/grpcio-1.76.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:45e0111e73f43f735d70786557dc38141185072d7ff8dc1829d6a77ac1471468", size = 11821522, upload-time = "2025-10-21T16:21:51.142Z" },
1044
- { url = "https://files.pythonhosted.org/packages/ac/31/2b8a235ab40c39cbc141ef647f8a6eb7b0028f023015a4842933bc0d6831/grpcio-1.76.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:83d57312a58dcfe2a3a0f9d1389b299438909a02db60e2f2ea2ae2d8034909d3", size = 6362558, upload-time = "2025-10-21T16:21:54.213Z" },
1045
- { url = "https://files.pythonhosted.org/packages/bd/64/9784eab483358e08847498ee56faf8ff6ea8e0a4592568d9f68edc97e9e9/grpcio-1.76.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:3e2a27c89eb9ac3d81ec8835e12414d73536c6e620355d65102503064a4ed6eb", size = 7049990, upload-time = "2025-10-21T16:21:56.476Z" },
1046
- { url = "https://files.pythonhosted.org/packages/2b/94/8c12319a6369434e7a184b987e8e9f3b49a114c489b8315f029e24de4837/grpcio-1.76.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:61f69297cba3950a524f61c7c8ee12e55c486cb5f7db47ff9dcee33da6f0d3ae", size = 6575387, upload-time = "2025-10-21T16:21:59.051Z" },
1047
- { url = "https://files.pythonhosted.org/packages/15/0f/f12c32b03f731f4a6242f771f63039df182c8b8e2cf8075b245b409259d4/grpcio-1.76.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6a15c17af8839b6801d554263c546c69c4d7718ad4321e3166175b37eaacca77", size = 7166668, upload-time = "2025-10-21T16:22:02.049Z" },
1048
- { url = "https://files.pythonhosted.org/packages/ff/2d/3ec9ce0c2b1d92dd59d1c3264aaec9f0f7c817d6e8ac683b97198a36ed5a/grpcio-1.76.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:25a18e9810fbc7e7f03ec2516addc116a957f8cbb8cbc95ccc80faa072743d03", size = 8124928, upload-time = "2025-10-21T16:22:04.984Z" },
1049
- { url = "https://files.pythonhosted.org/packages/1a/74/fd3317be5672f4856bcdd1a9e7b5e17554692d3db9a3b273879dc02d657d/grpcio-1.76.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:931091142fd8cc14edccc0845a79248bc155425eee9a98b2db2ea4f00a235a42", size = 7589983, upload-time = "2025-10-21T16:22:07.881Z" },
1050
- { url = "https://files.pythonhosted.org/packages/45/bb/ca038cf420f405971f19821c8c15bcbc875505f6ffadafe9ffd77871dc4c/grpcio-1.76.0-cp313-cp313-win32.whl", hash = "sha256:5e8571632780e08526f118f74170ad8d50fb0a48c23a746bef2a6ebade3abd6f", size = 3984727, upload-time = "2025-10-21T16:22:10.032Z" },
1051
- { url = "https://files.pythonhosted.org/packages/41/80/84087dc56437ced7cdd4b13d7875e7439a52a261e3ab4e06488ba6173b0a/grpcio-1.76.0-cp313-cp313-win_amd64.whl", hash = "sha256:f9f7bd5faab55f47231ad8dba7787866b69f5e93bc306e3915606779bbfb4ba8", size = 4702799, upload-time = "2025-10-21T16:22:12.709Z" },
1052
  ]
1053
 
1054
  [[package]]
@@ -1158,18 +1122,6 @@ wheels = [
1158
  { url = "https://files.pythonhosted.org/packages/cb/bd/1a875e0d592d447cbc02805fd3fe0f497714d6a2583f59d14fa9ebad96eb/huggingface_hub-0.36.0-py3-none-any.whl", hash = "sha256:7bcc9ad17d5b3f07b57c78e79d527102d08313caa278a641993acddcb894548d", size = 566094, upload-time = "2025-10-23T12:11:59.557Z" },
1159
  ]
1160
 
1161
- [[package]]
1162
- name = "humanfriendly"
1163
- version = "10.0"
1164
- source = { registry = "https://pypi.org/simple" }
1165
- dependencies = [
1166
- { name = "pyreadline3", marker = "sys_platform == 'win32'" },
1167
- ]
1168
- sdist = { url = "https://files.pythonhosted.org/packages/cc/3f/2c29224acb2e2df4d2046e4c73ee2662023c58ff5b113c4c1adac0886c43/humanfriendly-10.0.tar.gz", hash = "sha256:6b0b831ce8f15f7300721aa49829fc4e83921a9a301cc7f606be6686a2288ddc", size = 360702, upload-time = "2021-09-17T21:40:43.31Z" }
1169
- wheels = [
1170
- { url = "https://files.pythonhosted.org/packages/f0/0f/310fb31e39e2d734ccaa2c0fb981ee41f7bd5056ce9bc29b2248bd569169/humanfriendly-10.0-py2.py3-none-any.whl", hash = "sha256:1697e1a8a8f550fd43c2865cd84542fc175a61dcb779b6fee18cf6b6ccba1477", size = 86794, upload-time = "2021-09-17T21:40:39.897Z" },
1171
- ]
1172
-
1173
  [[package]]
1174
  name = "idna"
1175
  version = "3.11"
@@ -1181,14 +1133,14 @@ wheels = [
1181
 
1182
  [[package]]
1183
  name = "importlib-metadata"
1184
- version = "8.7.0"
1185
  source = { registry = "https://pypi.org/simple" }
1186
  dependencies = [
1187
  { name = "zipp" },
1188
  ]
1189
- sdist = { url = "https://files.pythonhosted.org/packages/76/66/650a33bd90f786193e4de4b3ad86ea60b53c89b669a5c7be931fac31cdb0/importlib_metadata-8.7.0.tar.gz", hash = "sha256:d13b81ad223b890aa16c5471f2ac3056cf76c5f10f82d6f9292f0b415f389000", size = 56641, upload-time = "2025-04-27T15:29:01.736Z" }
1190
  wheels = [
1191
- { url = "https://files.pythonhosted.org/packages/20/b0/36bd937216ec521246249be3bf9855081de4c5e06a0c9b4219dbeda50373/importlib_metadata-8.7.0-py3-none-any.whl", hash = "sha256:e5dd1551894c77868a30651cef00984d50e1002d06942a7101d34870c5f02afd", size = 27656, upload-time = "2025-04-27T15:29:00.214Z" },
1192
  ]
1193
 
1194
  [[package]]
@@ -1608,12 +1560,11 @@ wheels = [
1608
 
1609
  [[package]]
1610
  name = "kubernetes"
1611
- version = "34.1.0"
1612
  source = { registry = "https://pypi.org/simple" }
1613
  dependencies = [
1614
  { name = "certifi" },
1615
  { name = "durationpy" },
1616
- { name = "google-auth" },
1617
  { name = "python-dateutil" },
1618
  { name = "pyyaml" },
1619
  { name = "requests" },
@@ -1622,9 +1573,9 @@ dependencies = [
1622
  { name = "urllib3" },
1623
  { name = "websocket-client" },
1624
  ]
1625
- sdist = { url = "https://files.pythonhosted.org/packages/ef/55/3f880ef65f559cbed44a9aa20d3bdbc219a2c3a3bac4a30a513029b03ee9/kubernetes-34.1.0.tar.gz", hash = "sha256:8fe8edb0b5d290a2f3ac06596b23f87c658977d46b5f8df9d0f4ea83d0003912", size = 1083771, upload-time = "2025-09-29T20:23:49.283Z" }
1626
  wheels = [
1627
- { url = "https://files.pythonhosted.org/packages/ca/ec/65f7d563aa4a62dd58777e8f6aa882f15db53b14eb29aba0c28a20f7eb26/kubernetes-34.1.0-py2.py3-none-any.whl", hash = "sha256:bffba2272534e224e6a7a74d582deb0b545b7c9879d2cd9e4aae9481d1f2cc2a", size = 2008380, upload-time = "2025-09-29T20:23:47.684Z" },
1628
  ]
1629
 
1630
  [[package]]
@@ -2092,10 +2043,9 @@ wheels = [
2092
 
2093
  [[package]]
2094
  name = "onnxruntime"
2095
- version = "1.23.2"
2096
  source = { registry = "https://pypi.org/simple" }
2097
  dependencies = [
2098
- { name = "coloredlogs" },
2099
  { name = "flatbuffers" },
2100
  { name = "numpy" },
2101
  { name = "packaging" },
@@ -2103,23 +2053,23 @@ dependencies = [
2103
  { name = "sympy" },
2104
  ]
2105
  wheels = [
2106
- { url = "https://files.pythonhosted.org/packages/44/be/467b00f09061572f022ffd17e49e49e5a7a789056bad95b54dfd3bee73ff/onnxruntime-1.23.2-cp311-cp311-macosx_13_0_arm64.whl", hash = "sha256:6f91d2c9b0965e86827a5ba01531d5b669770b01775b23199565d6c1f136616c", size = 17196113, upload-time = "2025-10-22T03:47:33.526Z" },
2107
- { url = "https://files.pythonhosted.org/packages/9f/a8/3c23a8f75f93122d2b3410bfb74d06d0f8da4ac663185f91866b03f7da1b/onnxruntime-1.23.2-cp311-cp311-macosx_13_0_x86_64.whl", hash = "sha256:87d8b6eaf0fbeb6835a60a4265fde7a3b60157cf1b2764773ac47237b4d48612", size = 19153857, upload-time = "2025-10-22T03:46:37.578Z" },
2108
- { url = "https://files.pythonhosted.org/packages/3f/d8/506eed9af03d86f8db4880a4c47cd0dffee973ef7e4f4cff9f1d4bcf7d22/onnxruntime-1.23.2-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bbfd2fca76c855317568c1b36a885ddea2272c13cb0e395002c402f2360429a6", size = 15220095, upload-time = "2025-10-22T03:46:24.769Z" },
2109
- { url = "https://files.pythonhosted.org/packages/e9/80/113381ba832d5e777accedc6cb41d10f9eca82321ae31ebb6bcede530cea/onnxruntime-1.23.2-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:da44b99206e77734c5819aa2142c69e64f3b46edc3bd314f6a45a932defc0b3e", size = 17372080, upload-time = "2025-10-22T03:47:00.265Z" },
2110
- { url = "https://files.pythonhosted.org/packages/3a/db/1b4a62e23183a0c3fe441782462c0ede9a2a65c6bbffb9582fab7c7a0d38/onnxruntime-1.23.2-cp311-cp311-win_amd64.whl", hash = "sha256:902c756d8b633ce0dedd889b7c08459433fbcf35e9c38d1c03ddc020f0648c6e", size = 13468349, upload-time = "2025-10-22T03:47:25.783Z" },
2111
- { url = "https://files.pythonhosted.org/packages/1b/9e/f748cd64161213adeef83d0cb16cb8ace1e62fa501033acdd9f9341fff57/onnxruntime-1.23.2-cp312-cp312-macosx_13_0_arm64.whl", hash = "sha256:b8f029a6b98d3cf5be564d52802bb50a8489ab73409fa9db0bf583eabb7c2321", size = 17195929, upload-time = "2025-10-22T03:47:36.24Z" },
2112
- { url = "https://files.pythonhosted.org/packages/91/9d/a81aafd899b900101988ead7fb14974c8a58695338ab6a0f3d6b0100f30b/onnxruntime-1.23.2-cp312-cp312-macosx_13_0_x86_64.whl", hash = "sha256:218295a8acae83905f6f1aed8cacb8e3eb3bd7513a13fe4ba3b2664a19fc4a6b", size = 19157705, upload-time = "2025-10-22T03:46:40.415Z" },
2113
- { url = "https://files.pythonhosted.org/packages/3c/35/4e40f2fba272a6698d62be2cd21ddc3675edfc1a4b9ddefcc4648f115315/onnxruntime-1.23.2-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:76ff670550dc23e58ea9bc53b5149b99a44e63b34b524f7b8547469aaa0dcb8c", size = 15226915, upload-time = "2025-10-22T03:46:27.773Z" },
2114
- { url = "https://files.pythonhosted.org/packages/ef/88/9cc25d2bafe6bc0d4d3c1db3ade98196d5b355c0b273e6a5dc09c5d5d0d5/onnxruntime-1.23.2-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f9b4ae77f8e3c9bee50c27bc1beede83f786fe1d52e99ac85aa8d65a01e9b77", size = 17382649, upload-time = "2025-10-22T03:47:02.782Z" },
2115
- { url = "https://files.pythonhosted.org/packages/c0/b4/569d298f9fc4d286c11c45e85d9ffa9e877af12ace98af8cab52396e8f46/onnxruntime-1.23.2-cp312-cp312-win_amd64.whl", hash = "sha256:25de5214923ce941a3523739d34a520aac30f21e631de53bba9174dc9c004435", size = 13470528, upload-time = "2025-10-22T03:47:28.106Z" },
2116
- { url = "https://files.pythonhosted.org/packages/3d/41/fba0cabccecefe4a1b5fc8020c44febb334637f133acefc7ec492029dd2c/onnxruntime-1.23.2-cp313-cp313-macosx_13_0_arm64.whl", hash = "sha256:2ff531ad8496281b4297f32b83b01cdd719617e2351ffe0dba5684fb283afa1f", size = 17196337, upload-time = "2025-10-22T03:46:35.168Z" },
2117
- { url = "https://files.pythonhosted.org/packages/fe/f9/2d49ca491c6a986acce9f1d1d5fc2099108958cc1710c28e89a032c9cfe9/onnxruntime-1.23.2-cp313-cp313-macosx_13_0_x86_64.whl", hash = "sha256:162f4ca894ec3de1a6fd53589e511e06ecdc3ff646849b62a9da7489dee9ce95", size = 19157691, upload-time = "2025-10-22T03:46:43.518Z" },
2118
- { url = "https://files.pythonhosted.org/packages/1c/a1/428ee29c6eaf09a6f6be56f836213f104618fb35ac6cc586ff0f477263eb/onnxruntime-1.23.2-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:45d127d6e1e9b99d1ebeae9bcd8f98617a812f53f46699eafeb976275744826b", size = 15226898, upload-time = "2025-10-22T03:46:30.039Z" },
2119
- { url = "https://files.pythonhosted.org/packages/f2/2b/b57c8a2466a3126dbe0a792f56ad7290949b02f47b86216cd47d857e4b77/onnxruntime-1.23.2-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8bace4e0d46480fbeeb7bbe1ffe1f080e6663a42d1086ff95c1551f2d39e7872", size = 17382518, upload-time = "2025-10-22T03:47:05.407Z" },
2120
- { url = "https://files.pythonhosted.org/packages/4a/93/aba75358133b3a941d736816dd392f687e7eab77215a6e429879080b76b6/onnxruntime-1.23.2-cp313-cp313-win_amd64.whl", hash = "sha256:1f9cc0a55349c584f083c1c076e611a7c35d5b867d5d6e6d6c823bf821978088", size = 13470276, upload-time = "2025-10-22T03:47:31.193Z" },
2121
- { url = "https://files.pythonhosted.org/packages/7c/3d/6830fa61c69ca8e905f237001dbfc01689a4e4ab06147020a4518318881f/onnxruntime-1.23.2-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9d2385e774f46ac38f02b3a91a91e30263d41b2f1f4f26ae34805b2a9ddef466", size = 15229610, upload-time = "2025-10-22T03:46:32.239Z" },
2122
- { url = "https://files.pythonhosted.org/packages/b6/ca/862b1e7a639460f0ca25fd5b6135fb42cf9deea86d398a92e44dfda2279d/onnxruntime-1.23.2-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e2b9233c4947907fd1818d0e581c049c41ccc39b2856cc942ff6d26317cee145", size = 17394184, upload-time = "2025-10-22T03:47:08.127Z" },
2123
  ]
2124
 
2125
  [[package]]
@@ -2500,17 +2450,17 @@ wheels = [
2500
 
2501
  [[package]]
2502
  name = "protobuf"
2503
- version = "6.33.2"
2504
  source = { registry = "https://pypi.org/simple" }
2505
- sdist = { url = "https://files.pythonhosted.org/packages/34/44/e49ecff446afeec9d1a66d6bbf9adc21e3c7cea7803a920ca3773379d4f6/protobuf-6.33.2.tar.gz", hash = "sha256:56dc370c91fbb8ac85bc13582c9e373569668a290aa2e66a590c2a0d35ddb9e4", size = 444296, upload-time = "2025-12-06T00:17:53.311Z" }
2506
  wheels = [
2507
- { url = "https://files.pythonhosted.org/packages/bc/91/1e3a34881a88697a7354ffd177e8746e97a722e5e8db101544b47e84afb1/protobuf-6.33.2-cp310-abi3-win32.whl", hash = "sha256:87eb388bd2d0f78febd8f4c8779c79247b26a5befad525008e49a6955787ff3d", size = 425603, upload-time = "2025-12-06T00:17:41.114Z" },
2508
- { url = "https://files.pythonhosted.org/packages/64/20/4d50191997e917ae13ad0a235c8b42d8c1ab9c3e6fd455ca16d416944355/protobuf-6.33.2-cp310-abi3-win_amd64.whl", hash = "sha256:fc2a0e8b05b180e5fc0dd1559fe8ebdae21a27e81ac77728fb6c42b12c7419b4", size = 436930, upload-time = "2025-12-06T00:17:43.278Z" },
2509
- { url = "https://files.pythonhosted.org/packages/b2/ca/7e485da88ba45c920fb3f50ae78de29ab925d9e54ef0de678306abfbb497/protobuf-6.33.2-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:d9b19771ca75935b3a4422957bc518b0cecb978b31d1dd12037b088f6bcc0e43", size = 427621, upload-time = "2025-12-06T00:17:44.445Z" },
2510
- { url = "https://files.pythonhosted.org/packages/7d/4f/f743761e41d3b2b2566748eb76bbff2b43e14d5fcab694f494a16458b05f/protobuf-6.33.2-cp39-abi3-manylinux2014_aarch64.whl", hash = "sha256:b5d3b5625192214066d99b2b605f5783483575656784de223f00a8d00754fc0e", size = 324460, upload-time = "2025-12-06T00:17:45.678Z" },
2511
- { url = "https://files.pythonhosted.org/packages/b1/fa/26468d00a92824020f6f2090d827078c09c9c587e34cbfd2d0c7911221f8/protobuf-6.33.2-cp39-abi3-manylinux2014_s390x.whl", hash = "sha256:8cd7640aee0b7828b6d03ae518b5b4806fdfc1afe8de82f79c3454f8aef29872", size = 339168, upload-time = "2025-12-06T00:17:46.813Z" },
2512
- { url = "https://files.pythonhosted.org/packages/56/13/333b8f421738f149d4fe5e49553bc2a2ab75235486259f689b4b91f96cec/protobuf-6.33.2-cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:1f8017c48c07ec5859106533b682260ba3d7c5567b1ca1f24297ce03384d1b4f", size = 323270, upload-time = "2025-12-06T00:17:48.253Z" },
2513
- { url = "https://files.pythonhosted.org/packages/0e/15/4f02896cc3df04fc465010a4c6a0cd89810f54617a32a70ef531ed75d61c/protobuf-6.33.2-py3-none-any.whl", hash = "sha256:7636aad9bb01768870266de5dc009de2d1b936771b38a793f73cbbf279c91c5c", size = 170501, upload-time = "2025-12-06T00:17:52.211Z" },
2514
  ]
2515
 
2516
  [[package]]
@@ -2592,27 +2542,6 @@ wheels = [
2592
  { url = "https://files.pythonhosted.org/packages/8e/37/efad0257dc6e593a18957422533ff0f87ede7c9c6ea010a2177d738fb82f/pure_eval-0.2.3-py3-none-any.whl", hash = "sha256:1db8e35b67b3d218d818ae653e27f06c3aa420901fa7b081ca98cbedc874e0d0", size = 11842, upload-time = "2024-07-21T12:58:20.04Z" },
2593
  ]
2594
 
2595
- [[package]]
2596
- name = "pyasn1"
2597
- version = "0.6.1"
2598
- source = { registry = "https://pypi.org/simple" }
2599
- sdist = { url = "https://files.pythonhosted.org/packages/ba/e9/01f1a64245b89f039897cb0130016d79f77d52669aae6ee7b159a6c4c018/pyasn1-0.6.1.tar.gz", hash = "sha256:6f580d2bdd84365380830acf45550f2511469f673cb4a5ae3857a3170128b034", size = 145322, upload-time = "2024-09-10T22:41:42.55Z" }
2600
- wheels = [
2601
- { url = "https://files.pythonhosted.org/packages/c8/f1/d6a797abb14f6283c0ddff96bbdd46937f64122b8c925cab503dd37f8214/pyasn1-0.6.1-py3-none-any.whl", hash = "sha256:0d632f46f2ba09143da3a8afe9e33fb6f92fa2320ab7e886e2d0f7672af84629", size = 83135, upload-time = "2024-09-11T16:00:36.122Z" },
2602
- ]
2603
-
2604
- [[package]]
2605
- name = "pyasn1-modules"
2606
- version = "0.4.2"
2607
- source = { registry = "https://pypi.org/simple" }
2608
- dependencies = [
2609
- { name = "pyasn1" },
2610
- ]
2611
- sdist = { url = "https://files.pythonhosted.org/packages/e9/e6/78ebbb10a8c8e4b61a59249394a4a594c1a7af95593dc933a349c8d00964/pyasn1_modules-0.4.2.tar.gz", hash = "sha256:677091de870a80aae844b1ca6134f54652fa2c8c5a52aa396440ac3106e941e6", size = 307892, upload-time = "2025-03-28T02:41:22.17Z" }
2612
- wheels = [
2613
- { url = "https://files.pythonhosted.org/packages/47/8d/d529b5d697919ba8c11ad626e835d4039be708a35b0d22de83a269a6682c/pyasn1_modules-0.4.2-py3-none-any.whl", hash = "sha256:29253a9207ce32b64c3ac6600edc75368f98473906e8fd1043bd6b5b1de2c14a", size = 181259, upload-time = "2025-03-28T02:41:19.028Z" },
2614
- ]
2615
-
2616
  [[package]]
2617
  name = "pybase64"
2618
  version = "1.4.3"
@@ -2863,9 +2792,12 @@ wheels = [
2863
 
2864
  [[package]]
2865
  name = "pypika"
2866
- version = "0.48.9"
2867
  source = { registry = "https://pypi.org/simple" }
2868
- sdist = { url = "https://files.pythonhosted.org/packages/c7/2c/94ed7b91db81d61d7096ac8f2d325ec562fc75e35f3baea8749c85b28784/PyPika-0.48.9.tar.gz", hash = "sha256:838836a61747e7c8380cd1b7ff638694b7a7335345d0f559b04b2cd832ad5378", size = 67259, upload-time = "2022-03-15T11:22:57.066Z" }
 
 
 
2869
 
2870
  [[package]]
2871
  name = "pyproject-hooks"
@@ -2876,15 +2808,6 @@ wheels = [
2876
  { url = "https://files.pythonhosted.org/packages/bd/24/12818598c362d7f300f18e74db45963dbcb85150324092410c8b49405e42/pyproject_hooks-1.2.0-py3-none-any.whl", hash = "sha256:9e5c6bfa8dcc30091c74b0cf803c81fdd29d94f01992a7707bc97babb1141913", size = 10216, upload-time = "2024-09-29T09:24:11.978Z" },
2877
  ]
2878
 
2879
- [[package]]
2880
- name = "pyreadline3"
2881
- version = "3.5.4"
2882
- source = { registry = "https://pypi.org/simple" }
2883
- sdist = { url = "https://files.pythonhosted.org/packages/0f/49/4cea918a08f02817aabae639e3d0ac046fef9f9180518a3ad394e22da148/pyreadline3-3.5.4.tar.gz", hash = "sha256:8d57d53039a1c75adba8e50dd3d992b28143480816187ea5efbd5c78e6c885b7", size = 99839, upload-time = "2024-09-19T02:40:10.062Z" }
2884
- wheels = [
2885
- { url = "https://files.pythonhosted.org/packages/5a/dc/491b7661614ab97483abf2056be1deee4dc2490ecbf7bff9ab5cdbac86e1/pyreadline3-3.5.4-py3-none-any.whl", hash = "sha256:eaf8e6cc3c49bcccf145fc6067ba8643d1df34d604a1ec0eccbf7a18e6d3fae6", size = 83178, upload-time = "2024-09-19T02:40:08.598Z" },
2886
- ]
2887
-
2888
  [[package]]
2889
  name = "pytest"
2890
  version = "9.0.2"
@@ -3368,18 +3291,6 @@ wheels = [
3368
  { url = "https://files.pythonhosted.org/packages/d1/b7/b95708304cd49b7b6f82fdd039f1748b66ec2b21d6a45180910802f1abf1/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:ac37f9f516c51e5753f27dfdef11a88330f04de2d564be3991384b2f3535d02e", size = 562191, upload-time = "2025-11-30T20:24:36.853Z" },
3369
  ]
3370
 
3371
- [[package]]
3372
- name = "rsa"
3373
- version = "4.9.1"
3374
- source = { registry = "https://pypi.org/simple" }
3375
- dependencies = [
3376
- { name = "pyasn1" },
3377
- ]
3378
- sdist = { url = "https://files.pythonhosted.org/packages/da/8a/22b7beea3ee0d44b1916c0c1cb0ee3af23b700b6da9f04991899d0c555d4/rsa-4.9.1.tar.gz", hash = "sha256:e7bdbfdb5497da4c07dfd35530e1a902659db6ff241e39d9953cad06ebd0ae75", size = 29034, upload-time = "2025-04-16T09:51:18.218Z" }
3379
- wheels = [
3380
- { url = "https://files.pythonhosted.org/packages/64/8d/0133e4eb4beed9e425d9a98ed6e081a55d195481b7632472be1af08d2f6b/rsa-4.9.1-py3-none-any.whl", hash = "sha256:68635866661c6836b8d39430f97a996acbd61bfa49406748ea243539fe239762", size = 34696, upload-time = "2025-04-16T09:51:17.142Z" },
3381
- ]
3382
-
3383
  [[package]]
3384
  name = "safehttpx"
3385
  version = "0.1.7"
 
54
 
55
  [package.metadata]
56
  requires-dist = [
57
+ { name = "chromadb", specifier = ">=1.5.2" },
58
  { name = "fastapi", extras = ["all"], specifier = ">=0.122.0" },
59
  { name = "gradio", specifier = ">=6.2.0" },
60
  { name = "langchain", specifier = ">=1.1.3" },
 
375
 
376
  [[package]]
377
  name = "build"
378
+ version = "1.4.0"
379
  source = { registry = "https://pypi.org/simple" }
380
  dependencies = [
381
  { name = "colorama", marker = "(os_name == 'nt' and platform_machine != 'aarch64' and sys_platform == 'linux') or (os_name == 'nt' and platform_python_implementation != 'CPython' and sys_platform == 'linux') or (os_name == 'nt' and sys_platform != 'darwin' and sys_platform != 'linux')" },
382
  { name = "packaging" },
383
  { name = "pyproject-hooks" },
384
  ]
385
+ sdist = { url = "https://files.pythonhosted.org/packages/42/18/94eaffda7b329535d91f00fe605ab1f1e5cd68b2074d03f255c7d250687d/build-1.4.0.tar.gz", hash = "sha256:f1b91b925aa322be454f8330c6fb48b465da993d1e7e7e6fa35027ec49f3c936", size = 50054, upload-time = "2026-01-08T16:41:47.696Z" }
 
 
 
 
 
 
 
 
 
386
  wheels = [
387
+ { url = "https://files.pythonhosted.org/packages/c5/0d/84a4380f930db0010168e0aa7b7a8fed9ba1835a8fbb1472bc6d0201d529/build-1.4.0-py3-none-any.whl", hash = "sha256:6a07c1b8eb6f2b311b96fcbdbce5dab5fe637ffda0fd83c9cac622e927501596", size = 24141, upload-time = "2026-01-08T16:41:46.453Z" },
388
  ]
389
 
390
  [[package]]
 
503
 
504
  [[package]]
505
  name = "chromadb"
506
+ version = "1.5.2"
507
  source = { registry = "https://pypi.org/simple" }
508
  dependencies = [
509
  { name = "bcrypt" },
 
534
  { name = "typing-extensions" },
535
  { name = "uvicorn", extra = ["standard"] },
536
  ]
537
+ sdist = { url = "https://files.pythonhosted.org/packages/9e/48/aa5906f9f817b73c9e87e085d3a64705d91b7bb4f76f4649b9379baea980/chromadb-1.5.2.tar.gz", hash = "sha256:4fc3535a0fcd45343f93d298591882f68e659f24ed319aef14094b168105f956", size = 2386235, upload-time = "2026-02-27T19:49:34.167Z" }
538
  wheels = [
539
+ { url = "https://files.pythonhosted.org/packages/6b/3b/36989e7ebfa2ee10a85deacd423989b07f9e3bd176846863ace1305e9460/chromadb-1.5.2-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:a898ab200f9a22a16751eed5444dac330f1f82184264e16d5420e41e0afe63e4", size = 20733964, upload-time = "2026-02-27T19:49:31.683Z" },
540
+ { url = "https://files.pythonhosted.org/packages/85/b3/db3e5a8a47106d339c3e109e73859647969a81e9c54ca15bc6dde6685c1e/chromadb-1.5.2-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:8e6a12adb34bf441f8cc368b6460fbc9e14bee5cf926f34e752da759d68dec56", size = 19993688, upload-time = "2026-02-27T19:49:28.5Z" },
541
+ { url = "https://files.pythonhosted.org/packages/c2/88/0a9b6dddac3097f321ec8b057d09a61b4edb2b42b891fce7c2bfd01cd4c3/chromadb-1.5.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b533db30303ce5a82856ded8897c3cafd3160e1f2dccf5473d0bfdee49a159b3", size = 20642212, upload-time = "2026-02-27T19:49:22.467Z" },
542
+ { url = "https://files.pythonhosted.org/packages/ae/74/b8cd9d9bc72c545a579fd1f7bb44558a801ed5f5bab164a25eea16d51ad9/chromadb-1.5.2-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e48e5b0f300d6f709446a5d9299614e3b6bca997772d810e1298b76b0c4e7dbb", size = 21529269, upload-time = "2026-02-27T19:49:25.699Z" },
543
+ { url = "https://files.pythonhosted.org/packages/e8/96/fa83f81f8b618ffca7527915f99cf054c6f8bd272bf3cf5c0616757083ba/chromadb-1.5.2-cp39-abi3-win_amd64.whl", hash = "sha256:042e746ee0c9db34eef2723c4dca30197ded3bf9d27846d996fd51715ec7b0e3", size = 21863829, upload-time = "2026-02-27T19:49:36.422Z" },
544
  ]
545
 
546
  [[package]]
 
564
  { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
565
  ]
566
 
 
 
 
 
 
 
 
 
 
 
 
 
567
  [[package]]
568
  name = "comm"
569
  version = "0.2.3"
 
825
 
826
  [[package]]
827
  name = "flatbuffers"
828
+ version = "25.12.19"
829
  source = { registry = "https://pypi.org/simple" }
 
830
  wheels = [
831
+ { url = "https://files.pythonhosted.org/packages/e8/2d/d2a548598be01649e2d46231d151a6c56d10b964d94043a335ae56ea2d92/flatbuffers-25.12.19-py2.py3-none-any.whl", hash = "sha256:7634f50c427838bb021c2d66a3d1168e9d199b0607e6329399f04846d42e20b4", size = 26661, upload-time = "2025-12-19T23:16:13.622Z" },
832
  ]
833
 
834
  [[package]]
 
849
  { url = "https://files.pythonhosted.org/packages/51/c7/b64cae5dba3a1b138d7123ec36bb5ccd39d39939f18454407e5468f4763f/fsspec-2025.12.0-py3-none-any.whl", hash = "sha256:8bf1fe301b7d8acfa6e8571e3b1c3d158f909666642431cc78a1b7b4dbc5ec5b", size = 201422, upload-time = "2025-12-03T15:23:41.434Z" },
850
  ]
851
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
852
  [[package]]
853
  name = "googleapis-common-protos"
854
  version = "1.72.0"
 
976
 
977
  [[package]]
978
  name = "grpcio"
979
+ version = "1.78.0"
980
  source = { registry = "https://pypi.org/simple" }
981
  dependencies = [
982
  { name = "typing-extensions" },
983
  ]
984
+ sdist = { url = "https://files.pythonhosted.org/packages/06/8a/3d098f35c143a89520e568e6539cc098fcd294495910e359889ce8741c84/grpcio-1.78.0.tar.gz", hash = "sha256:7382b95189546f375c174f53a5fa873cef91c4b8005faa05cc5b3beea9c4f1c5", size = 12852416, upload-time = "2026-02-06T09:57:18.093Z" }
985
+ wheels = [
986
+ { url = "https://files.pythonhosted.org/packages/86/c7/d0b780a29b0837bf4ca9580904dfb275c1fc321ded7897d620af7047ec57/grpcio-1.78.0-cp311-cp311-linux_armv7l.whl", hash = "sha256:2777b783f6c13b92bd7b716667452c329eefd646bfb3f2e9dabea2e05dbd34f6", size = 5951525, upload-time = "2026-02-06T09:55:01.989Z" },
987
+ { url = "https://files.pythonhosted.org/packages/c5/b1/96920bf2ee61df85a9503cb6f733fe711c0ff321a5a697d791b075673281/grpcio-1.78.0-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:9dca934f24c732750389ce49d638069c3892ad065df86cb465b3fa3012b70c9e", size = 11830418, upload-time = "2026-02-06T09:55:04.462Z" },
988
+ { url = "https://files.pythonhosted.org/packages/83/0c/7c1528f098aeb75a97de2bae18c530f56959fb7ad6c882db45d9884d6edc/grpcio-1.78.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:459ab414b35f4496138d0ecd735fed26f1318af5e52cb1efbc82a09f0d5aa911", size = 6524477, upload-time = "2026-02-06T09:55:07.111Z" },
989
+ { url = "https://files.pythonhosted.org/packages/8d/52/e7c1f3688f949058e19a011c4e0dec973da3d0ae5e033909677f967ae1f4/grpcio-1.78.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:082653eecbdf290e6e3e2c276ab2c54b9e7c299e07f4221872380312d8cf395e", size = 7198266, upload-time = "2026-02-06T09:55:10.016Z" },
990
+ { url = "https://files.pythonhosted.org/packages/e5/61/8ac32517c1e856677282c34f2e7812d6c328fa02b8f4067ab80e77fdc9c9/grpcio-1.78.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:85f93781028ec63f383f6bc90db785a016319c561cc11151fbb7b34e0d012303", size = 6730552, upload-time = "2026-02-06T09:55:12.207Z" },
991
+ { url = "https://files.pythonhosted.org/packages/bd/98/b8ee0158199250220734f620b12e4a345955ac7329cfd908d0bf0fda77f0/grpcio-1.78.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:f12857d24d98441af6a1d5c87442d624411db486f7ba12550b07788f74b67b04", size = 7304296, upload-time = "2026-02-06T09:55:15.044Z" },
992
+ { url = "https://files.pythonhosted.org/packages/bd/0f/7b72762e0d8840b58032a56fdbd02b78fc645b9fa993d71abf04edbc54f4/grpcio-1.78.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5397fff416b79e4b284959642a4e95ac4b0f1ece82c9993658e0e477d40551ec", size = 8288298, upload-time = "2026-02-06T09:55:17.276Z" },
993
+ { url = "https://files.pythonhosted.org/packages/24/ae/ae4ce56bc5bb5caa3a486d60f5f6083ac3469228faa734362487176c15c5/grpcio-1.78.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:fbe6e89c7ffb48518384068321621b2a69cab509f58e40e4399fdd378fa6d074", size = 7730953, upload-time = "2026-02-06T09:55:19.545Z" },
994
+ { url = "https://files.pythonhosted.org/packages/b5/6e/8052e3a28eb6a820c372b2eb4b5e32d195c661e137d3eca94d534a4cfd8a/grpcio-1.78.0-cp311-cp311-win32.whl", hash = "sha256:6092beabe1966a3229f599d7088b38dfc8ffa1608b5b5cdda31e591e6500f856", size = 4076503, upload-time = "2026-02-06T09:55:21.521Z" },
995
+ { url = "https://files.pythonhosted.org/packages/08/62/f22c98c5265dfad327251fa2f840b591b1df5f5e15d88b19c18c86965b27/grpcio-1.78.0-cp311-cp311-win_amd64.whl", hash = "sha256:1afa62af6e23f88629f2b29ec9e52ec7c65a7176c1e0a83292b93c76ca882558", size = 4799767, upload-time = "2026-02-06T09:55:24.107Z" },
996
+ { url = "https://files.pythonhosted.org/packages/4e/f4/7384ed0178203d6074446b3c4f46c90a22ddf7ae0b3aee521627f54cfc2a/grpcio-1.78.0-cp312-cp312-linux_armv7l.whl", hash = "sha256:f9ab915a267fc47c7e88c387a3a28325b58c898e23d4995f765728f4e3dedb97", size = 5913985, upload-time = "2026-02-06T09:55:26.832Z" },
997
+ { url = "https://files.pythonhosted.org/packages/81/ed/be1caa25f06594463f685b3790b320f18aea49b33166f4141bfdc2bfb236/grpcio-1.78.0-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:3f8904a8165ab21e07e58bf3e30a73f4dffc7a1e0dbc32d51c61b5360d26f43e", size = 11811853, upload-time = "2026-02-06T09:55:29.224Z" },
998
+ { url = "https://files.pythonhosted.org/packages/24/a7/f06d151afc4e64b7e3cc3e872d331d011c279aaab02831e40a81c691fb65/grpcio-1.78.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:859b13906ce098c0b493af92142ad051bf64c7870fa58a123911c88606714996", size = 6475766, upload-time = "2026-02-06T09:55:31.825Z" },
999
+ { url = "https://files.pythonhosted.org/packages/8a/a8/4482922da832ec0082d0f2cc3a10976d84a7424707f25780b82814aafc0a/grpcio-1.78.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:b2342d87af32790f934a79c3112641e7b27d63c261b8b4395350dad43eff1dc7", size = 7170027, upload-time = "2026-02-06T09:55:34.7Z" },
1000
+ { url = "https://files.pythonhosted.org/packages/54/bf/f4a3b9693e35d25b24b0b39fa46d7d8a3c439e0a3036c3451764678fec20/grpcio-1.78.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:12a771591ae40bc65ba67048fa52ef4f0e6db8279e595fd349f9dfddeef571f9", size = 6690766, upload-time = "2026-02-06T09:55:36.902Z" },
1001
+ { url = "https://files.pythonhosted.org/packages/c7/b9/521875265cc99fe5ad4c5a17010018085cae2810a928bf15ebe7d8bcd9cc/grpcio-1.78.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:185dea0d5260cbb2d224c507bf2a5444d5abbb1fa3594c1ed7e4c709d5eb8383", size = 7266161, upload-time = "2026-02-06T09:55:39.824Z" },
1002
+ { url = "https://files.pythonhosted.org/packages/05/86/296a82844fd40a4ad4a95f100b55044b4f817dece732bf686aea1a284147/grpcio-1.78.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:51b13f9aed9d59ee389ad666b8c2214cc87b5de258fa712f9ab05f922e3896c6", size = 8253303, upload-time = "2026-02-06T09:55:42.353Z" },
1003
+ { url = "https://files.pythonhosted.org/packages/f3/e4/ea3c0caf5468537f27ad5aab92b681ed7cc0ef5f8c9196d3fd42c8c2286b/grpcio-1.78.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fd5f135b1bd58ab088930b3c613455796dfa0393626a6972663ccdda5b4ac6ce", size = 7698222, upload-time = "2026-02-06T09:55:44.629Z" },
1004
+ { url = "https://files.pythonhosted.org/packages/d7/47/7f05f81e4bb6b831e93271fb12fd52ba7b319b5402cbc101d588f435df00/grpcio-1.78.0-cp312-cp312-win32.whl", hash = "sha256:94309f498bcc07e5a7d16089ab984d42ad96af1d94b5a4eb966a266d9fcabf68", size = 4066123, upload-time = "2026-02-06T09:55:47.644Z" },
1005
+ { url = "https://files.pythonhosted.org/packages/ad/e7/d6914822c88aa2974dbbd10903d801a28a19ce9cd8bad7e694cbbcf61528/grpcio-1.78.0-cp312-cp312-win_amd64.whl", hash = "sha256:9566fe4ababbb2610c39190791e5b829869351d14369603702e890ef3ad2d06e", size = 4797657, upload-time = "2026-02-06T09:55:49.86Z" },
1006
+ { url = "https://files.pythonhosted.org/packages/05/a9/8f75894993895f361ed8636cd9237f4ab39ef87fd30db17467235ed1c045/grpcio-1.78.0-cp313-cp313-linux_armv7l.whl", hash = "sha256:ce3a90455492bf8bfa38e56fbbe1dbd4f872a3d8eeaf7337dc3b1c8aa28c271b", size = 5920143, upload-time = "2026-02-06T09:55:52.035Z" },
1007
+ { url = "https://files.pythonhosted.org/packages/55/06/0b78408e938ac424100100fd081189451b472236e8a3a1f6500390dc4954/grpcio-1.78.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:2bf5e2e163b356978b23652c4818ce4759d40f4712ee9ec5a83c4be6f8c23a3a", size = 11803926, upload-time = "2026-02-06T09:55:55.494Z" },
1008
+ { url = "https://files.pythonhosted.org/packages/88/93/b59fe7832ff6ae3c78b813ea43dac60e295fa03606d14d89d2e0ec29f4f3/grpcio-1.78.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8f2ac84905d12918e4e55a16da17939eb63e433dc11b677267c35568aa63fc84", size = 6478628, upload-time = "2026-02-06T09:55:58.533Z" },
1009
+ { url = "https://files.pythonhosted.org/packages/ed/df/e67e3734527f9926b7d9c0dde6cd998d1d26850c3ed8eeec81297967ac67/grpcio-1.78.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:b58f37edab4a3881bc6c9bca52670610e0c9ca14e2ea3cf9debf185b870457fb", size = 7173574, upload-time = "2026-02-06T09:56:01.786Z" },
1010
+ { url = "https://files.pythonhosted.org/packages/a6/62/cc03fffb07bfba982a9ec097b164e8835546980aec25ecfa5f9c1a47e022/grpcio-1.78.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:735e38e176a88ce41840c21bb49098ab66177c64c82426e24e0082500cc68af5", size = 6692639, upload-time = "2026-02-06T09:56:04.529Z" },
1011
+ { url = "https://files.pythonhosted.org/packages/bf/9a/289c32e301b85bdb67d7ec68b752155e674ee3ba2173a1858f118e399ef3/grpcio-1.78.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2045397e63a7a0ee7957c25f7dbb36ddc110e0cfb418403d110c0a7a68a844e9", size = 7268838, upload-time = "2026-02-06T09:56:08.397Z" },
1012
+ { url = "https://files.pythonhosted.org/packages/0e/79/1be93f32add280461fa4773880196572563e9c8510861ac2da0ea0f892b6/grpcio-1.78.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:a9f136fbafe7ccf4ac7e8e0c28b31066e810be52d6e344ef954a3a70234e1702", size = 8251878, upload-time = "2026-02-06T09:56:10.914Z" },
1013
+ { url = "https://files.pythonhosted.org/packages/65/65/793f8e95296ab92e4164593674ae6291b204bb5f67f9d4a711489cd30ffa/grpcio-1.78.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:748b6138585379c737adc08aeffd21222abbda1a86a0dca2a39682feb9196c20", size = 7695412, upload-time = "2026-02-06T09:56:13.593Z" },
1014
+ { url = "https://files.pythonhosted.org/packages/1c/9f/1e233fe697ecc82845942c2822ed06bb522e70d6771c28d5528e4c50f6a4/grpcio-1.78.0-cp313-cp313-win32.whl", hash = "sha256:271c73e6e5676afe4fc52907686670c7cea22ab2310b76a59b678403ed40d670", size = 4064899, upload-time = "2026-02-06T09:56:15.601Z" },
1015
+ { url = "https://files.pythonhosted.org/packages/4d/27/d86b89e36de8a951501fb06a0f38df19853210f341d0b28f83f4aa0ffa08/grpcio-1.78.0-cp313-cp313-win_amd64.whl", hash = "sha256:f2d4e43ee362adfc05994ed479334d5a451ab7bc3f3fee1b796b8ca66895acb4", size = 4797393, upload-time = "2026-02-06T09:56:17.882Z" },
1016
  ]
1017
 
1018
  [[package]]
 
1122
  { url = "https://files.pythonhosted.org/packages/cb/bd/1a875e0d592d447cbc02805fd3fe0f497714d6a2583f59d14fa9ebad96eb/huggingface_hub-0.36.0-py3-none-any.whl", hash = "sha256:7bcc9ad17d5b3f07b57c78e79d527102d08313caa278a641993acddcb894548d", size = 566094, upload-time = "2025-10-23T12:11:59.557Z" },
1123
  ]
1124
 
 
 
 
 
 
 
 
 
 
 
 
 
1125
  [[package]]
1126
  name = "idna"
1127
  version = "3.11"
 
1133
 
1134
  [[package]]
1135
  name = "importlib-metadata"
1136
+ version = "8.7.1"
1137
  source = { registry = "https://pypi.org/simple" }
1138
  dependencies = [
1139
  { name = "zipp" },
1140
  ]
1141
+ sdist = { url = "https://files.pythonhosted.org/packages/f3/49/3b30cad09e7771a4982d9975a8cbf64f00d4a1ececb53297f1d9a7be1b10/importlib_metadata-8.7.1.tar.gz", hash = "sha256:49fef1ae6440c182052f407c8d34a68f72efc36db9ca90dc0113398f2fdde8bb", size = 57107, upload-time = "2025-12-21T10:00:19.278Z" }
1142
  wheels = [
1143
+ { url = "https://files.pythonhosted.org/packages/fa/5e/f8e9a1d23b9c20a551a8a02ea3637b4642e22c2626e3a13a9a29cdea99eb/importlib_metadata-8.7.1-py3-none-any.whl", hash = "sha256:5a1f80bf1daa489495071efbb095d75a634cf28a8bc299581244063b53176151", size = 27865, upload-time = "2025-12-21T10:00:18.329Z" },
1144
  ]
1145
 
1146
  [[package]]
 
1560
 
1561
  [[package]]
1562
  name = "kubernetes"
1563
+ version = "35.0.0"
1564
  source = { registry = "https://pypi.org/simple" }
1565
  dependencies = [
1566
  { name = "certifi" },
1567
  { name = "durationpy" },
 
1568
  { name = "python-dateutil" },
1569
  { name = "pyyaml" },
1570
  { name = "requests" },
 
1573
  { name = "urllib3" },
1574
  { name = "websocket-client" },
1575
  ]
1576
+ sdist = { url = "https://files.pythonhosted.org/packages/2c/8f/85bf51ad4150f64e8c665daf0d9dfe9787ae92005efb9a4d1cba592bd79d/kubernetes-35.0.0.tar.gz", hash = "sha256:3d00d344944239821458b9efd484d6df9f011da367ecb155dadf9513f05f09ee", size = 1094642, upload-time = "2026-01-16T01:05:27.76Z" }
1577
  wheels = [
1578
+ { url = "https://files.pythonhosted.org/packages/0c/70/05b685ea2dffcb2adbf3cdcea5d8865b7bc66f67249084cf845012a0ff13/kubernetes-35.0.0-py2.py3-none-any.whl", hash = "sha256:39e2b33b46e5834ef6c3985ebfe2047ab39135d41de51ce7641a7ca5b372a13d", size = 2017602, upload-time = "2026-01-16T01:05:25.991Z" },
1579
  ]
1580
 
1581
  [[package]]
 
2043
 
2044
  [[package]]
2045
  name = "onnxruntime"
2046
+ version = "1.24.2"
2047
  source = { registry = "https://pypi.org/simple" }
2048
  dependencies = [
 
2049
  { name = "flatbuffers" },
2050
  { name = "numpy" },
2051
  { name = "packaging" },
 
2053
  { name = "sympy" },
2054
  ]
2055
  wheels = [
2056
+ { url = "https://files.pythonhosted.org/packages/2c/4e/050c947924ffd8ff856d219d8f83ee3d4e7dc52d5a6770ff34a15675c437/onnxruntime-1.24.2-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:69d1c75997276106d24e65da2e69ec4302af1b117fef414e2154740cde0f6214", size = 17217298, upload-time = "2026-02-19T17:15:09.891Z" },
2057
+ { url = "https://files.pythonhosted.org/packages/30/17/c814121dff4de962476ced979c402c3cce72d5d46e87099610b47a1f2622/onnxruntime-1.24.2-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:670d7e671af2dbd17638472f9b9ff98041889efd7150718406b9ea989312d064", size = 15027128, upload-time = "2026-02-19T17:13:19.367Z" },
2058
+ { url = "https://files.pythonhosted.org/packages/2c/32/4e5921ba8b82ac37cad45f1108ca6effd430f49c7f20577d53f317d166ed/onnxruntime-1.24.2-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:93fe190ee555ae8e9c1214bcfcf13af85cd06dd835e8d835ce5a8d01056844fe", size = 17107440, upload-time = "2026-02-19T17:14:02.932Z" },
2059
+ { url = "https://files.pythonhosted.org/packages/48/55/9d13c97d912db81e81c9b369a49b36f2804fa3bb8de64462e5e6bd412d0b/onnxruntime-1.24.2-cp311-cp311-win_amd64.whl", hash = "sha256:04a3a80b28dd39739463cb1e34081eed668929ba0b8e1bc861885dcdf66b7601", size = 12506375, upload-time = "2026-02-19T17:14:57.049Z" },
2060
+ { url = "https://files.pythonhosted.org/packages/b0/d4/cf0e0b3bd84e7b68fe911810f7098f414936d1ffb612faa569a3fb8a76a5/onnxruntime-1.24.2-cp311-cp311-win_arm64.whl", hash = "sha256:a845096277444670b0b52855bb4aad706003540bd34986b50868e9f29606c142", size = 12167758, upload-time = "2026-02-19T17:14:47.386Z" },
2061
+ { url = "https://files.pythonhosted.org/packages/23/1c/38af1cfe82c75d2b205eb5019834b0f2b0b6647ec8a20a3086168e413570/onnxruntime-1.24.2-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:d8a50b422d45c0144864c0977d04ad4fa50a8a48e5153056ab1f7d06ea9fc3e2", size = 17217857, upload-time = "2026-02-19T17:15:14.297Z" },
2062
+ { url = "https://files.pythonhosted.org/packages/01/8a/e2d4332ae18d6383376e75141cd914256bee12c3cc439f42260eb176ceb9/onnxruntime-1.24.2-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:76c44fc9a89dcefcd5a4ab5c6bbbb9ff1604325ab2d5d0bc9ff5a9cba7b37f4a", size = 15027167, upload-time = "2026-02-19T17:13:21.92Z" },
2063
+ { url = "https://files.pythonhosted.org/packages/35/af/ad86cfbfd65d5a86204b3a30893e92c0cf3f1a56280efc5a12e69d81f52d/onnxruntime-1.24.2-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:09aa6f8d766b4afc3cfba68dd10be39586b49f9462fbd1386c5d5644239461ca", size = 17106547, upload-time = "2026-02-19T17:14:05.758Z" },
2064
+ { url = "https://files.pythonhosted.org/packages/ee/62/9d725326f933bf8323e309956a17e52d33fb59d35bb5dda1886f94352938/onnxruntime-1.24.2-cp312-cp312-win_amd64.whl", hash = "sha256:ebcee9276420a65e5fa08b05f18379c2271b5992617e5bdc0d0d6c5ea395c1a1", size = 12506161, upload-time = "2026-02-19T17:14:59.377Z" },
2065
+ { url = "https://files.pythonhosted.org/packages/aa/a9/7b06efd5802db881860d961a7cb4efacb058ed694c1c8f096c0c1499d017/onnxruntime-1.24.2-cp312-cp312-win_arm64.whl", hash = "sha256:8d770a934513f6e17937baf3438eaaec5983a23cdaedb81c9fc0dfcf26831c24", size = 12169884, upload-time = "2026-02-19T17:14:49.962Z" },
2066
+ { url = "https://files.pythonhosted.org/packages/9c/98/8f5b9ae63f7f6dd5fb2d192454b915ec966a421fdd0effeeef5be7f7221f/onnxruntime-1.24.2-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:038ebcd8363c3835ea83eed66129e1d11d8219438892dfb7dc7656c4d4dfa1f9", size = 17217884, upload-time = "2026-02-19T17:13:36.193Z" },
2067
+ { url = "https://files.pythonhosted.org/packages/55/e6/dc4dc59565c93506c45017c0dd3f536f6d1b7bc97047821af13fba2e3def/onnxruntime-1.24.2-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8235cc11e118ad749c497ba93288c04073eccd8cc6cc508c8a7988ae36ab52d8", size = 15026995, upload-time = "2026-02-19T17:13:25.029Z" },
2068
+ { url = "https://files.pythonhosted.org/packages/ac/62/6f2851cf3237a91bc04cdb35434293a623d4f6369f79836929600da574ba/onnxruntime-1.24.2-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e92b46cc6d8be4286436a05382a881c88d85a2ae1ea9cfe5e6fab89f2c3e89cc", size = 17106308, upload-time = "2026-02-19T17:14:09.817Z" },
2069
+ { url = "https://files.pythonhosted.org/packages/62/5a/1e2b874daf24f26e98af14281fdbdd6ae1ed548ba471c01ea2a3084c55bb/onnxruntime-1.24.2-cp313-cp313-win_amd64.whl", hash = "sha256:1fd824ee4f6fb811bc47ffec2b25f129f31a087214ca91c8b4f6fda32962b78f", size = 12506095, upload-time = "2026-02-19T17:15:02.434Z" },
2070
+ { url = "https://files.pythonhosted.org/packages/2d/6f/8fac5eecb94f861d56a43ede3c2ebcdce60132952d3b72003f3e3d91483c/onnxruntime-1.24.2-cp313-cp313-win_arm64.whl", hash = "sha256:d8cf0acbf90771fff012c33eb2749e8aca2a8b4c66c672f30ee77c140a6fba5b", size = 12168564, upload-time = "2026-02-19T17:14:52.28Z" },
2071
+ { url = "https://files.pythonhosted.org/packages/35/e4/7dfed3f445f7289a0abff709d012439c6c901915390704dd918e5f47aad3/onnxruntime-1.24.2-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e22fb5d9ac51b61f50cca155ce2927576cc2c42501ede6c0df23a1aeb070bdd5", size = 15036844, upload-time = "2026-02-19T17:13:27.928Z" },
2072
+ { url = "https://files.pythonhosted.org/packages/90/45/9d52397e30b0d8c1692afcec5184ca9372ff4d6b0f6039bba9ad479a2563/onnxruntime-1.24.2-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2956f5220e7be8b09482ae5726caabf78eb549142cdb28523191a38e57fb6119", size = 17117779, upload-time = "2026-02-19T17:14:13.862Z" },
2073
  ]
2074
 
2075
  [[package]]
 
2450
 
2451
  [[package]]
2452
  name = "protobuf"
2453
+ version = "6.33.5"
2454
  source = { registry = "https://pypi.org/simple" }
2455
+ sdist = { url = "https://files.pythonhosted.org/packages/ba/25/7c72c307aafc96fa87062aa6291d9f7c94836e43214d43722e86037aac02/protobuf-6.33.5.tar.gz", hash = "sha256:6ddcac2a081f8b7b9642c09406bc6a4290128fce5f471cddd165960bb9119e5c", size = 444465, upload-time = "2026-01-29T21:51:33.494Z" }
2456
  wheels = [
2457
+ { url = "https://files.pythonhosted.org/packages/b1/79/af92d0a8369732b027e6d6084251dd8e782c685c72da161bd4a2e00fbabb/protobuf-6.33.5-cp310-abi3-win32.whl", hash = "sha256:d71b040839446bac0f4d162e758bea99c8251161dae9d0983a3b88dee345153b", size = 425769, upload-time = "2026-01-29T21:51:21.751Z" },
2458
+ { url = "https://files.pythonhosted.org/packages/55/75/bb9bc917d10e9ee13dee8607eb9ab963b7cf8be607c46e7862c748aa2af7/protobuf-6.33.5-cp310-abi3-win_amd64.whl", hash = "sha256:3093804752167bcab3998bec9f1048baae6e29505adaf1afd14a37bddede533c", size = 437118, upload-time = "2026-01-29T21:51:24.022Z" },
2459
+ { url = "https://files.pythonhosted.org/packages/a2/6b/e48dfc1191bc5b52950246275bf4089773e91cb5ba3592621723cdddca62/protobuf-6.33.5-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:a5cb85982d95d906df1e2210e58f8e4f1e3cdc088e52c921a041f9c9a0386de5", size = 427766, upload-time = "2026-01-29T21:51:25.413Z" },
2460
+ { url = "https://files.pythonhosted.org/packages/4e/b1/c79468184310de09d75095ed1314b839eb2f72df71097db9d1404a1b2717/protobuf-6.33.5-cp39-abi3-manylinux2014_aarch64.whl", hash = "sha256:9b71e0281f36f179d00cbcb119cb19dec4d14a81393e5ea220f64b286173e190", size = 324638, upload-time = "2026-01-29T21:51:26.423Z" },
2461
+ { url = "https://files.pythonhosted.org/packages/c5/f5/65d838092fd01c44d16037953fd4c2cc851e783de9b8f02b27ec4ffd906f/protobuf-6.33.5-cp39-abi3-manylinux2014_s390x.whl", hash = "sha256:8afa18e1d6d20af15b417e728e9f60f3aa108ee76f23c3b2c07a2c3b546d3afd", size = 339411, upload-time = "2026-01-29T21:51:27.446Z" },
2462
+ { url = "https://files.pythonhosted.org/packages/9b/53/a9443aa3ca9ba8724fdfa02dd1887c1bcd8e89556b715cfbacca6b63dbec/protobuf-6.33.5-cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:cbf16ba3350fb7b889fca858fb215967792dc125b35c7976ca4818bee3521cf0", size = 323465, upload-time = "2026-01-29T21:51:28.925Z" },
2463
+ { url = "https://files.pythonhosted.org/packages/57/bf/2086963c69bdac3d7cff1cc7ff79b8ce5ea0bec6797a017e1be338a46248/protobuf-6.33.5-py3-none-any.whl", hash = "sha256:69915a973dd0f60f31a08b8318b73eab2bd6a392c79184b3612226b0a3f8ec02", size = 170687, upload-time = "2026-01-29T21:51:32.557Z" },
2464
  ]
2465
 
2466
  [[package]]
 
2542
  { url = "https://files.pythonhosted.org/packages/8e/37/efad0257dc6e593a18957422533ff0f87ede7c9c6ea010a2177d738fb82f/pure_eval-0.2.3-py3-none-any.whl", hash = "sha256:1db8e35b67b3d218d818ae653e27f06c3aa420901fa7b081ca98cbedc874e0d0", size = 11842, upload-time = "2024-07-21T12:58:20.04Z" },
2543
  ]
2544
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2545
  [[package]]
2546
  name = "pybase64"
2547
  version = "1.4.3"
 
2792
 
2793
  [[package]]
2794
  name = "pypika"
2795
+ version = "0.51.1"
2796
  source = { registry = "https://pypi.org/simple" }
2797
+ sdist = { url = "https://files.pythonhosted.org/packages/f8/78/cbaebba88e05e2dcda13ca203131b38d3640219f20ebb49676d26714861b/pypika-0.51.1.tar.gz", hash = "sha256:c30c7c1048fbf056fd3920c5a2b88b0c29dd190a9b2bee971fd17e4abe4d0ebe", size = 80919, upload-time = "2026-02-04T11:27:48.304Z" }
2798
+ wheels = [
2799
+ { url = "https://files.pythonhosted.org/packages/57/83/c77dfeed04022e8930b08eedca2b6e5efed256ab3321396fde90066efb65/pypika-0.51.1-py2.py3-none-any.whl", hash = "sha256:77985b4d7ce71b9905255bf12468cf598349e98837c037541cfc240e528aec46", size = 60585, upload-time = "2026-02-04T11:27:46.251Z" },
2800
+ ]
2801
 
2802
  [[package]]
2803
  name = "pyproject-hooks"
 
2808
  { url = "https://files.pythonhosted.org/packages/bd/24/12818598c362d7f300f18e74db45963dbcb85150324092410c8b49405e42/pyproject_hooks-1.2.0-py3-none-any.whl", hash = "sha256:9e5c6bfa8dcc30091c74b0cf803c81fdd29d94f01992a7707bc97babb1141913", size = 10216, upload-time = "2024-09-29T09:24:11.978Z" },
2809
  ]
2810
 
 
 
 
 
 
 
 
 
 
2811
  [[package]]
2812
  name = "pytest"
2813
  version = "9.0.2"
 
3291
  { url = "https://files.pythonhosted.org/packages/d1/b7/b95708304cd49b7b6f82fdd039f1748b66ec2b21d6a45180910802f1abf1/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:ac37f9f516c51e5753f27dfdef11a88330f04de2d564be3991384b2f3535d02e", size = 562191, upload-time = "2025-11-30T20:24:36.853Z" },
3292
  ]
3293
 
 
 
 
 
 
 
 
 
 
 
 
 
3294
  [[package]]
3295
  name = "safehttpx"
3296
  version = "0.1.7"