Pushkar Niroula commited on
Commit
21dca42
·
unverified ·
2 Parent(s): f24939c55943c5

Merge pull request #2 from Pushkar222-n/feature/OptimizePrompt

Browse files
config.py ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pydantic_settings import BaseSettings
2
+ import os
3
+
4
+
5
+ class Settings(BaseSettings):
6
+ groq_api_key: str
7
+ neo4j_uri: str
8
+ neo4j_username: str
9
+ neo4j_password: str
10
+ neo4j_database: str
11
+ aura_instanceid: str
12
+ aura_instancename: str
13
+ model_name: str
14
+
15
+ class Config:
16
+ env_file = '.env'
17
+
18
+
19
+ settings = Settings() # type: ignore
src/api/main.py CHANGED
@@ -4,6 +4,8 @@ from pydantic import BaseModel, Field
4
  import uvicorn
5
  from src.api.schemas import RecommendationRequest, RecommendationResponse
6
  import time
 
 
7
 
8
  from src.retrieval.rag_pipeline import AnimeRAGPipeline
9
 
@@ -78,13 +80,14 @@ async def get_recommendations(request: RecommendationRequest):
78
  recommendations=result["recommendations"],
79
  retrieved_count=result["retrieved_count"],
80
  metadata={
81
- "model": "llama-3.3-70b-versatile",
82
  "retriever_k": rag_pipeline.retriever_k,
83
  "Time taken for LLM + vector search": str(end_time - start_time)
84
  }
85
  )
86
 
87
  except Exception as e:
 
88
  raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
89
  detail=f"Error processing request: {str(e)}")
90
 
@@ -97,7 +100,7 @@ async def get_stats():
97
  return {
98
  "total_anime": rag_pipeline.retriever.collection.count(),
99
  "embedding_model": "all-MiniLM-L6-v2",
100
- "llm_model": "llama-3.1-70b-versatile",
101
  "retrieval_k": rag_pipeline.retriever_k
102
  }
103
 
 
4
  import uvicorn
5
  from src.api.schemas import RecommendationRequest, RecommendationResponse
6
  import time
7
+ from config import settings
8
+ import traceback
9
 
10
  from src.retrieval.rag_pipeline import AnimeRAGPipeline
11
 
 
80
  recommendations=result["recommendations"],
81
  retrieved_count=result["retrieved_count"],
82
  metadata={
83
+ "model": settings.model_name,
84
  "retriever_k": rag_pipeline.retriever_k,
85
  "Time taken for LLM + vector search": str(end_time - start_time)
86
  }
87
  )
88
 
89
  except Exception as e:
90
+ traceback.print_exc()
91
  raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
92
  detail=f"Error processing request: {str(e)}")
93
 
 
100
  return {
101
  "total_anime": rag_pipeline.retriever.collection.count(),
102
  "embedding_model": "all-MiniLM-L6-v2",
103
+ "llm_model": settings.model_name,
104
  "retrieval_k": rag_pipeline.retriever_k
105
  }
106
 
src/data_ingestion/load_to_neo4j.py ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from neo4j import GraphDatabase
2
+ import pandas as pd
3
+ from src.utils.neo4j_client import Neo4j_Client
4
+
5
+ class AnimeGraphLoader:
6
+ """Loads anime into Neo4j Graph"""
7
+
8
+ def __init__(self):
9
+ self.client = Neo4j_Client()
10
+
11
+ def clear_database(self):
12
+ """Delete all nodes (use carefully !)"""
13
+ query = "MATCH (n) DETACH DELETE n"
14
+ self.client.run_query(query)
15
+
16
+ def create_constraints(self):
17
+ """Create uniqueness constraints for performance"""
18
+ queries = [
19
+ "CREATE CONSTRAINT anime_id IF NOT EXISTS FOR (a: Anime) REQUIRE a.mal_id IS UNIQUE",
20
+ "CREATE CONSTRAINT genre_name IF NOT EXISTS FOR (g:Genre) REQUIRE g.name IS UNIQUE",
21
+ "CREATE CONSTRAINT theme_name IF NOT EXISTS FOR (t:Theme) REQUIRE t.name IS UNIQUE",
22
+ "CREATE CONSTRAINT demographic_name IF NOT EXISTS FOR (d:Demographic) REQUIRE d.name IS UNIQUE",
23
+ "CREAE CONSTRAINT content_type IF NOT EXISTS FOR (t: Type) REQUIRE t.type IS UNIUE"
24
+ ]
src/llm/groq_client.py CHANGED
@@ -2,15 +2,15 @@ import os
2
  from groq import Groq
3
  from dotenv import load_dotenv
4
  import logging
 
5
 
6
- load_dotenv()
7
  logger = logging.getLogger(__name__)
8
 
9
 
10
  class GroqLLM:
11
  """Wrapper for Groq API"""
12
 
13
- def __init__(self, model: str = "llama-3.3-70b-versatile"):
14
  """
15
  Initialize Groq Client
16
 
@@ -19,7 +19,7 @@ class GroqLLM:
19
  - llama-3.1-8b-instant: Faster, good enough for most tasks
20
  """
21
 
22
- self.client = Groq()
23
  self.model = model
24
 
25
  logger.info(f"Initialized Groq with model {self.model}")
 
2
  from groq import Groq
3
  from dotenv import load_dotenv
4
  import logging
5
+ from config import settings
6
 
 
7
  logger = logging.getLogger(__name__)
8
 
9
 
10
  class GroqLLM:
11
  """Wrapper for Groq API"""
12
 
13
+ def __init__(self, model: str = "llama-3.1-8b-instant"):
14
  """
15
  Initialize Groq Client
16
 
 
19
  - llama-3.1-8b-instant: Faster, good enough for most tasks
20
  """
21
 
22
+ self.client = Groq(api_key=settings.groq_api_key)
23
  self.model = model
24
 
25
  logger.info(f"Initialized Groq with model {self.model}")
src/llm/prompts.py CHANGED
@@ -28,16 +28,16 @@ def create_recommendation_prompt(
28
  context = "\n".join(context_parts)
29
 
30
  prompt = f"""
31
- You are an expert anime recommendation assistant. A user has asked for recommendations, and I've retrieved some potentially relevant anime from the database.
32
  First: If the user is asking for a comparison or opinion on specific anime,
33
  provide thoughtful comparison rather than a list of recommendations.
34
 
35
- If recommendations, onlt then:
36
  Your task is to:
37
  1. Analyze the user's request carefully, paying attention to specific preferences (tone, themes, etc.)
38
  2. Evaluate each retrieved anime against their user's criteria
39
  3. Select the {n_recommendations} BEST matches that truly fit what they're asking for
40
- 4. Explain why each recommendation fits their request
41
 
42
  User's Query:
43
  "{user_query}"
@@ -50,6 +50,8 @@ Instructions:
50
  - Don't just list all retrieved anime - SELECT the best {n_recommendations} that truly match
51
  - For each recommendation, explain in 1-2 sentences WHY it matches their request
52
  - If some retrieved anime DON'T match the user's specific criteria, exclude them
 
 
53
  - Be honest if none of the retrieved anime are great matches
54
 
55
  Format your response as:
@@ -61,7 +63,13 @@ Format your response as:
61
 
62
  [Continue for {n_recommendations} recommendations]
63
 
64
- If you think the retrieved anime don't match the request well, say so and explain what type of anime would be better."""
 
 
 
 
 
 
65
 
66
  return prompt
67
 
 
28
  context = "\n".join(context_parts)
29
 
30
  prompt = f"""
31
+ You are an expert anime recommendation assistant. A user has asked for recommendations, and you are provided in context relevant similar anime from the database.
32
  First: If the user is asking for a comparison or opinion on specific anime,
33
  provide thoughtful comparison rather than a list of recommendations.
34
 
35
+ ## 1. If recommendations, only then:
36
  Your task is to:
37
  1. Analyze the user's request carefully, paying attention to specific preferences (tone, themes, etc.)
38
  2. Evaluate each retrieved anime against their user's criteria
39
  3. Select the {n_recommendations} BEST matches that truly fit what they're asking for
40
+ 4. Explain why each recommendation fits their request. Answer in a way, that sounds really casual, super relaxed, and don't explain your "system thinking" such as "User has asked for ...., So here they are..." or any such variations.
41
 
42
  User's Query:
43
  "{user_query}"
 
50
  - Don't just list all retrieved anime - SELECT the best {n_recommendations} that truly match
51
  - For each recommendation, explain in 1-2 sentences WHY it matches their request
52
  - If some retrieved anime DON'T match the user's specific criteria, exclude them
53
+ - If some retrieved anime are the sequel (can be identified by name or other information) do not mention them, UNLESS user specifically asks.
54
+ -> If user SPEICIFICALLY Ask about some sequel, and there's no synopsis, tell the User something similar to Synopsis of prequel(that might be in context) and additionally answer based on other featurs like genre, demographics, etc. DO NOT INVENT SYNOPSIS !!!
55
  - Be honest if none of the retrieved anime are great matches
56
 
57
  Format your response as:
 
63
 
64
  [Continue for {n_recommendations} recommendations]
65
 
66
+ If you think the retrieved anime don't match the request well, say so and explain what type of anime would be better.
67
+
68
+ ## 2. If the user wants to talk casually about some anime they recently watched, engage into meaningful conversation with them, and ONLY when it seems user wants recommendations, give recommendation like mentioned in ##1 above.
69
+ ## 3. FOR ANY OTHER Gibberish queries, not matching to the "Anime world" except some occasional GREETINGS(in which case you have to greet properly), just give a generic message that "You are not capable of answering that, politely".
70
+
71
+ **Response Style**: Friendly, Casual, and do not mention the programmer(or anything similar to what/how you process/look for data). Just a Intelligent Bot answering to a user.
72
+ """
73
 
74
  return prompt
75
 
src/retrieval/rag_pipeline.py CHANGED
@@ -2,6 +2,7 @@ from src.retrieval.vector_search import AnimeRetriever
2
  from src.llm.groq_client import GroqLLM
3
  from src.llm.prompts import create_recommendation_prompt, create_system_prompt
4
  import logging
 
5
 
6
  logger = logging.getLogger(__name__)
7
 
@@ -24,7 +25,7 @@ class AnimeRAGPipeline:
24
  recommendation_n: How many to recommend in final output
25
  """
26
  self.retriever = retriever or AnimeRetriever()
27
- self.llm = llm or GroqLLM(model="llama-3.3-70b-versatile")
28
  self.retriever_k = retriever_k
29
  self.recommendation_n = recommendation_n
30
 
 
2
  from src.llm.groq_client import GroqLLM
3
  from src.llm.prompts import create_recommendation_prompt, create_system_prompt
4
  import logging
5
+ from config import settings
6
 
7
  logger = logging.getLogger(__name__)
8
 
 
25
  recommendation_n: How many to recommend in final output
26
  """
27
  self.retriever = retriever or AnimeRetriever()
28
+ self.llm = llm or GroqLLM(model=settings.model_name)
29
  self.retriever_k = retriever_k
30
  self.recommendation_n = recommendation_n
31
 
src/retrieval/vector_search.py CHANGED
@@ -52,14 +52,15 @@ class AnimeRetriever:
52
  distance = results["distances"][0][i] # type: ignore
53
 
54
  # Genre filtering (if specified)
55
- if genre_filter:
56
- if genre_filter.lower() not in metadata["genres"].lower(): # type: ignore
 
57
  continue
58
 
59
  anime_info = {
60
  "mal_id": results["ids"][0][i],
61
  "title": metadata["title"],
62
- "genres": metadata["genres"],
63
  "score": metadata["score"],
64
  "type": metadata["type"],
65
  "year": metadata["year"],
@@ -106,4 +107,5 @@ if __name__ == "__main__":
106
  print("\n=== Test 4: Scored by Filter ===")
107
  results = retriever.search("adventure", n_results=5, min_score=8.0)
108
  for anime in results:
109
- print(f"- {anime['title']} (score: {anime['score']}) (scored_by: {anime['scored_by']})")
 
 
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"],
 
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']})")
src/utils/neo4j_client.py CHANGED
@@ -1,17 +1,16 @@
1
- import os
2
  from dotenv import load_dotenv
3
  from neo4j import GraphDatabase
4
 
5
- load_dotenv()
6
 
7
 
8
  class Neo4j_Client:
9
  """Neo4j database client"""
10
 
11
  def __init__(self):
12
- uri = os.getenv("NEO4J_URI")
13
- user = os.getenv("NEO4J_USERNAME")
14
- password = os.getenv("NEO4J_PASSWORD")
15
  try:
16
  self.driver = GraphDatabase.driver(
17
  uri, auth=(user, password)) # type: ignore
 
 
1
  from dotenv import load_dotenv
2
  from neo4j import GraphDatabase
3
 
4
+ from config import settings
5
 
6
 
7
  class Neo4j_Client:
8
  """Neo4j database client"""
9
 
10
  def __init__(self):
11
+ uri = settings.neo4j_uri
12
+ user = settings.neo4j_username
13
+ password = settings.neo4j_password
14
  try:
15
  self.driver = GraphDatabase.driver(
16
  uri, auth=(user, password)) # type: ignore
ui/gradio_app.py CHANGED
@@ -1,3 +1,117 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import gradio as gr
2
  import requests
3
  import os
@@ -5,98 +119,405 @@ import os
5
  API_URL = os.getenv("API_URL", "http://127.0.0.1:8000")
6
 
7
 
8
- def get_recommendations(query, min_score, genre_filter):
9
- """Call the fastapi backend"""
10
  try:
11
- payload = {
12
- "query": query,
13
- "n_results": 5
14
- }
15
-
16
  if min_score > 0:
17
  payload["min_score"] = min_score
18
-
19
- if genre_filter and genre_filter != None:
20
  payload["genre_filter"] = genre_filter
21
 
22
  response = requests.post(
23
- f"{API_URL}/recommend",
24
- json=payload,
25
- timeout=30
26
- )
27
  response.raise_for_status()
28
-
29
  result = response.json()
30
  print(result["metadata"])
31
  return result["recommendations"]
32
  except requests.exceptions.RequestException as e:
33
- return f"Error connecting to the API. Make sure FastAPI server is running. \nDetails: {str(e)}"
34
  except Exception as e:
35
- return f"Error: {str(e)}"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
36
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
37
 
38
- with gr.Blocks(title="Anime Recommender") as demo:
39
- gr.Markdown("""
40
- # Anime Recommendation System
41
-
42
- Powered by RAG(Retrieval-Augmented Generation)
43
-
44
- Ask for anime recommendations and get AI-powered suggestions!
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
45
  """)
 
 
 
 
 
 
 
 
 
 
 
46
  with gr.Row():
47
- with gr.Column(scale=2):
48
- query_input = gr.Textbox(
49
- label="What are you looking for?",
50
- placeholder="e.g., 'Anime similar to Death Note but lighter' or 'Romantic comedy set in high school'",
51
- lines=3
52
- )
53
-
54
- with gr.Row():
55
- min_score_slider = gr.Slider(
56
- minimum=0,
57
- maximum=10,
58
- value=0,
59
- step=0.5,
60
- label="Minimum Rating this animes should have (0 = no filter)"
61
- )
62
-
63
- genre_dropdown = gr.Dropdown(
64
- choices=["None", "Action", "Comedy", "Drama",
65
- "Romance", "Sci-Fi", "Fantasy", "Thriller"],
66
- value="None",
67
- label="Genre Filter (optional)"
68
- )
69
-
70
- submit_btn = gr.Button("Get Recommendations",
71
- variant="primary", size="lg")
72
-
73
- with gr.Column(scale=3):
74
- output = gr.Markdown(label="Recommendations")
 
 
 
75
 
76
  # Examples
 
77
  gr.Examples(
78
  examples=[
79
- ["Anime similar to Death Note but lighter", 0, "None"],
80
- ["Romantic comedy set in high school", 7.5, "Comedy"],
81
- ["Dark psychological thriller", 8.0, "None"],
82
- ["Action anime with epic fights", 7.0, "Action"],
 
83
  ],
84
- inputs=[query_input, min_score_slider, genre_dropdown],
 
 
85
  )
86
 
87
- # Connect button to function
88
- submit_btn.click(
89
- fn=get_recommendations,
90
- inputs=[query_input, min_score_slider, genre_dropdown],
91
- outputs=output
92
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
93
 
94
- # Launch
95
  if __name__ == "__main__":
96
- print("Starting Gradio UI...")
97
- print("Make sure FastAPI server is running at http://localhost:8000")
98
- demo.launch(
99
- server_name="0.0.0.0",
100
- server_port=7860,
101
- share=True # Set to True to get public URL
102
- )
 
1
+ # import gradio as gr
2
+ # import requests
3
+ # import os
4
+
5
+ # API_URL = os.getenv("API_URL", "http://127.0.0.1:8000")
6
+
7
+
8
+ # def get_recommendations(query, min_score, genre_filter, n_results):
9
+ # """Call the fastapi backend"""
10
+ # try:
11
+ # payload = {
12
+ # "query": query,
13
+ # "n_results": n_results
14
+ # }
15
+
16
+ # if min_score > 0:
17
+ # payload["min_score"] = min_score
18
+
19
+ # if genre_filter and genre_filter != None:
20
+ # payload["genre_filter"] = genre_filter
21
+
22
+ # response = requests.post(
23
+ # f"{API_URL}/recommend",
24
+ # json=payload,
25
+ # timeout=30
26
+ # )
27
+ # response.raise_for_status()
28
+
29
+ # result = response.json()
30
+ # print(result["metadata"])
31
+ # return result["recommendations"]
32
+ # except requests.exceptions.RequestException as e:
33
+ # return f"Error connecting to the API. Make sure FastAPI server is running. \nDetails: {str(e)}"
34
+ # except Exception as e:
35
+ # return f"Error: {str(e)}"
36
+
37
+
38
+ # with gr.Blocks(title="Anime Recommender") as demo:
39
+ # gr.Markdown("""
40
+ # # Anime Recommendation System
41
+
42
+ # Powered by RAG(Retrieval-Augmented Generation)
43
+
44
+ # Ask for anime recommendations and get AI-powered suggestions!
45
+ # """)
46
+ # with gr.Row():
47
+ # with gr.Column(scale=2):
48
+ # query_input = gr.Textbox(
49
+ # label="What are you looking for?",
50
+ # placeholder="e.g., 'Anime similar to Death Note but lighter' or 'Romantic comedy set in high school'",
51
+ # lines=3
52
+ # )
53
+
54
+ # with gr.Row():
55
+ # min_score_slider = gr.Slider(
56
+ # minimum=0,
57
+ # maximum=10,
58
+ # value=0,
59
+ # step=0.5,
60
+ # label="Minimum Rating this animes should have (0 = no filter)"
61
+ # )
62
+
63
+ # genre_dropdown = gr.Dropdown(
64
+ # choices=["None", "Action", "Comedy", "Drama",
65
+ # "Romance", "Sci-Fi", "Fantasy", "Thriller"],
66
+ # value="None",
67
+ # label="Genre Filter (optional)"
68
+ # )
69
+
70
+ # n_results_dropdown = gr.Slider(
71
+ # minimum=1,
72
+ # maximum=8,
73
+ # value=3,
74
+ # step=1,
75
+ # label="Number of recommendation you want to get"
76
+ # )
77
+
78
+ # submit_btn = gr.Button("Get Recommendations",
79
+ # variant="primary", size="lg")
80
+
81
+ # with gr.Column(scale=3):
82
+ # output = gr.Markdown(label="Recommendations")
83
+
84
+ # # Examples
85
+ # gr.Examples(
86
+ # examples=[
87
+ # ["Anime similar to Death Note but lighter", 0, "None", 3],
88
+ # ["Romantic comedy set in high school", 7.5, "Comedy", 4],
89
+ # ["Dark psychological thriller", 8.0, "None", 2],
90
+ # ["Action anime with epic fights", 7.0, "Action", 1],
91
+ # ],
92
+ # inputs=[query_input, min_score_slider,
93
+ # genre_dropdown, n_results_dropdown],
94
+ # )
95
+
96
+ # # Connect button to function
97
+ # submit_btn.click(
98
+ # fn=get_recommendations,
99
+ # inputs=[query_input, min_score_slider,
100
+ # genre_dropdown, n_results_dropdown],
101
+ # outputs=output
102
+ # )
103
+
104
+ # # Launch
105
+ # if __name__ == "__main__":
106
+ # print("Starting Gradio UI...")
107
+ # print("Make sure FastAPI server is running at http://localhost:8000")
108
+ # demo.launch(
109
+ # server_name="0.0.0.0",
110
+ # server_port=7860,
111
+ # share=True # Set to True to get public URL
112
+ # )
113
+
114
+
115
  import gradio as gr
116
  import requests
117
  import os
 
119
  API_URL = os.getenv("API_URL", "http://127.0.0.1:8000")
120
 
121
 
122
+ def get_recommendations(query, min_score, genre_filter, n_results):
 
123
  try:
124
+ payload = {"query": query, "n_results": int(n_results)}
 
 
 
 
125
  if min_score > 0:
126
  payload["min_score"] = min_score
127
+ if genre_filter and genre_filter != "None":
 
128
  payload["genre_filter"] = genre_filter
129
 
130
  response = requests.post(
131
+ f"{API_URL}/recommend", json=payload, timeout=30)
 
 
 
132
  response.raise_for_status()
 
133
  result = response.json()
134
  print(result["metadata"])
135
  return result["recommendations"]
136
  except requests.exceptions.RequestException as e:
137
+ return f"**Connection Error** Make sure FastAPI is running.\n\n`{str(e)}`"
138
  except Exception as e:
139
+ return f"**Error:** {str(e)}"
140
+
141
+
142
+ css = """
143
+ @import url('https://fonts.googleapis.com/css2?family=DM+Sans:wght@300;400;500;600&family=DM+Serif+Display:ital@0;1&display=swap');
144
+
145
+ :root {
146
+ --bg: #1c1714;
147
+ --surface: #241e1a;
148
+ --card: #2d2520;
149
+ --border: #3d3028;
150
+ --accent: #d4845a;
151
+ --gold: #c9a97a;
152
+ --text: #f5ede3;
153
+ --sub: #d0bfae;
154
+ --muted: #9a8878;
155
+ --faint: #6a5a4a;
156
+ --radius: 16px;
157
+ --shadow: 0 4px 28px rgba(0,0,0,0.4);
158
+ }
159
+
160
+ /* ── GLOBAL ── */
161
+ body, .gradio-container {
162
+ background: var(--bg) !important;
163
+ font-family: 'DM Sans', sans-serif !important;
164
+ color: var(--text) !important;
165
+ }
166
+ footer { display: none !important; }
167
+ .gradio-container {
168
+ max-width: 980px !important;
169
+ margin: 0 auto !important;
170
+ padding: 0 36px !important;
171
+ }
172
+
173
+ /* ── HEADER ── */
174
+ .app-header { text-align: center; padding: 56px 0 40px; }
175
+ .app-header h1 {
176
+ font-family: 'DM Serif Display', serif !important;
177
+ font-size: 46px !important;
178
+ font-weight: 400 !important;
179
+ color: var(--text) !important;
180
+ margin: 0 0 8px !important;
181
+ letter-spacing: -0.5px;
182
+ }
183
+ .app-header h1 em { font-style: italic; color: var(--accent); }
184
+ .app-header p { font-size: 15px; color: var(--sub); margin: 0; }
185
+ .header-line { width: 44px; height: 2px; background: var(--accent); border-radius: 2px; margin: 16px auto 0; opacity: 0.75; }
186
 
187
+ /* ── STEP CARDS ── */
188
+ .step-card {
189
+ background: var(--card);
190
+ border-radius: var(--radius);
191
+ padding: 28px 32px 26px;
192
+ margin-bottom: 14px;
193
+ box-shadow: var(--shadow);
194
+ border: 1px solid var(--border);
195
+ }
196
+ .step-label {
197
+ font-size: 11px; font-weight: 600; letter-spacing: 0.18em;
198
+ text-transform: uppercase; color: var(--muted);
199
+ margin-bottom: 16px; display: flex; align-items: center; gap: 10px;
200
+ }
201
+ .step-label .num {
202
+ width: 21px; height: 21px; background: var(--accent); color: #1c1714;
203
+ border-radius: 50%; display: inline-flex; align-items: center;
204
+ justify-content: center; font-size: 10px; font-weight: 700;
205
+ }
206
 
207
+ /* ── INPUTS ── */
208
+ textarea, input[type="text"] {
209
+ background: var(--surface) !important;
210
+ border: 1.5px solid var(--border) !important;
211
+ border-radius: 10px !important;
212
+ color: var(--text) !important;
213
+ font-family: 'DM Sans', sans-serif !important;
214
+ font-size: 15px !important;
215
+ transition: border-color 0.2s, box-shadow 0.2s !important;
216
+ }
217
+ textarea::placeholder, input::placeholder { color: var(--faint) !important; }
218
+ textarea:focus, input:focus {
219
+ border-color: var(--accent) !important;
220
+ box-shadow: 0 0 0 3px rgba(212,132,90,0.15) !important;
221
+ outline: none !important;
222
+ }
223
+ label > span {
224
+ font-size: 13px !important; font-weight: 500 !important;
225
+ color: var(--sub) !important; letter-spacing: 0 !important;
226
+ text-transform: none !important;
227
+ }
228
+
229
+ /* ── SLIDER ── */
230
+ input[type="range"] { accent-color: var(--accent) !important; }
231
+
232
+ /* ── SUBMIT BUTTON ── */
233
+ #submit-btn button {
234
+ width: 100% !important;
235
+ background: var(--accent) !important;
236
+ color: #1c1714 !important;
237
+ border: none !important;
238
+ border-radius: 12px !important;
239
+ font-family: 'DM Sans', sans-serif !important;
240
+ font-size: 15px !important;
241
+ font-weight: 700 !important;
242
+ padding: 17px !important;
243
+ cursor: pointer !important;
244
+ letter-spacing: 0.03em !important;
245
+ transition: background 0.2s, transform 0.15s, box-shadow 0.2s !important;
246
+ box-shadow: 0 4px 20px rgba(212,132,90,0.30) !important;
247
+ }
248
+ #submit-btn button:hover {
249
+ background: var(--gold) !important;
250
+ transform: translateY(-2px) !important;
251
+ box-shadow: 0 8px 28px rgba(212,132,90,0.42) !important;
252
+ }
253
+ #submit-btn button:active { transform: translateY(0) !important; }
254
+
255
+ /* ── LOADER ── */
256
+ #ao-loader { display: none; text-align: center; padding: 44px 0 36px; }
257
+ .ao-loader-phrase {
258
+ font-family: 'DM Serif Display', serif;
259
+ font-size: 20px; font-style: italic;
260
+ color: var(--text); margin-bottom: 24px;
261
+ min-height: 30px; transition: opacity 0.35s;
262
+ }
263
+ .ao-dots { display: flex; justify-content: center; gap: 10px; }
264
+ .ao-dots span {
265
+ width: 9px; height: 9px; background: var(--accent);
266
+ border-radius: 50%; animation: aobounce 1.2s infinite ease-in-out;
267
+ }
268
+ .ao-dots span:nth-child(2) { animation-delay: 0.18s; background: var(--gold); }
269
+ .ao-dots span:nth-child(3) { animation-delay: 0.36s; background: var(--faint); }
270
+ @keyframes aobounce {
271
+ 0%, 80%, 100% { transform: translateY(0) scale(0.75); opacity: 0.35; }
272
+ 40% { transform: translateY(-11px) scale(1.15); opacity: 1; }
273
+ }
274
+
275
+ /* ── OUTPUT BOX ── */
276
+ /* Target the Gradio Markdown block container via elem_id */
277
+ #ao-output-wrap,
278
+ #ao-output-wrap > .wrap,
279
+ div#ao-output-wrap {
280
+ background: var(--card) !important;
281
+ border-radius: var(--radius) !important;
282
+ border: 1px solid var(--border) !important;
283
+ box-shadow: var(--shadow) !important;
284
+ padding: 32px 36px !important;
285
+ margin-top: 4px !important;
286
+ transition: opacity 0.45s !important;
287
+ }
288
+ /* The actual prose inside Markdown */
289
+ #ao-output-wrap .prose,
290
+ #ao-output-wrap .md,
291
+ #ao-output-wrap > div {
292
+ color: var(--sub) !important;
293
+ }
294
+ #ao-output-wrap p,
295
+ #ao-output-wrap .prose p {
296
+ font-size: 14.5px !important;
297
+ line-height: 1.78 !important;
298
+ color: var(--sub) !important;
299
+ margin: 0 0 10px !important;
300
+ }
301
+ #ao-output-wrap h1, #ao-output-wrap h2,
302
+ #ao-output-wrap .prose h1, #ao-output-wrap .prose h2 {
303
+ font-family: 'DM Serif Display', serif !important;
304
+ font-size: 22px !important;
305
+ font-weight: 400 !important;
306
+ color: var(--text) !important;
307
+ padding-bottom: 10px !important;
308
+ border-bottom: 1px solid var(--border) !important;
309
+ margin: 28px 0 12px !important;
310
+ }
311
+ #ao-output-wrap h2:first-child,
312
+ #ao-output-wrap .prose h2:first-child { margin-top: 0 !important; }
313
+ #ao-output-wrap h3,
314
+ #ao-output-wrap .prose h3 {
315
+ font-size: 15px !important;
316
+ color: var(--accent) !important;
317
+ font-weight: 600 !important;
318
+ margin: 0 0 5px !important;
319
+ }
320
+ #ao-output-wrap strong,
321
+ #ao-output-wrap .prose strong { color: var(--text) !important; font-weight: 600 !important; }
322
+ #ao-output-wrap em,
323
+ #ao-output-wrap .prose em { color: var(--gold) !important; }
324
+ #ao-output-wrap code,
325
+ #ao-output-wrap .prose code {
326
+ background: var(--surface) !important;
327
+ color: var(--accent) !important;
328
+ padding: 2px 7px !important;
329
+ border-radius: 5px !important;
330
+ font-size: 13px !important;
331
+ border: 1px solid var(--border) !important;
332
+ }
333
+ #ao-output-wrap ul,
334
+ #ao-output-wrap .prose ul { padding-left: 18px !important; }
335
+ #ao-output-wrap li,
336
+ #ao-output-wrap .prose li {
337
+ font-size: 14.5px !important;
338
+ line-height: 1.72 !important;
339
+ color: var(--sub) !important;
340
+ margin-bottom: 4px !important;
341
+ }
342
+
343
+ /* ── EXAMPLES ── */
344
+ .examples-label {
345
+ font-size: 11px; font-weight: 600; letter-spacing: 0.18em;
346
+ text-transform: uppercase; color: var(--faint); margin: 26px 0 12px;
347
+ }
348
+ .gr-samples-table td, .examples-holder td {
349
+ background: var(--card) !important;
350
+ border: 1px solid var(--border) !important;
351
+ border-radius: 8px !important;
352
+ color: var(--sub) !important;
353
+ font-size: 13px !important; padding: 10px 15px !important;
354
+ transition: all 0.15s !important; cursor: pointer !important;
355
+ font-family: 'DM Sans', sans-serif !important;
356
+ }
357
+ .gr-samples-table tr:hover td, .examples-holder tr:hover td {
358
+ background: var(--accent) !important;
359
+ color: #1c1714 !important;
360
+ border-color: var(--accent) !important;
361
+ }
362
+ .gr-samples-header, .gr-samples-header th { display: none !important; }
363
+ label[data-testid="block-label"] { display: none !important; }
364
+ .examples-holder > label { display: none !important; }
365
+
366
+ /* ── SCROLLBAR ── */
367
+ ::-webkit-scrollbar { width: 5px; }
368
+ ::-webkit-scrollbar-track { background: var(--bg); }
369
+ ::-webkit-scrollbar-thumb { background: var(--border); border-radius: 4px; }
370
+ ::-webkit-scrollbar-thumb:hover { background: var(--faint); }
371
+ """
372
+
373
+
374
+ with gr.Blocks(
375
+ title="Anime Oracle",
376
+ css=css,
377
+ theme=gr.themes.Base(
378
+ primary_hue="orange",
379
+ neutral_hue="slate",
380
+ font=gr.themes.GoogleFont("DM Sans"),
381
+ ).set(
382
+ body_background_fill="#1c1714",
383
+ body_text_color="#f5ede3",
384
+ block_background_fill="#2d2520",
385
+ block_border_color="#3d3028",
386
+ input_background_fill="#241e1a",
387
+ input_border_color="#3d3028",
388
+ button_primary_background_fill="#d4845a",
389
+ button_primary_text_color="#1c1714",
390
+ )
391
+ ) as demo:
392
+
393
+ gr.HTML("""
394
+ <div class="app-header">
395
+ <h1>Find your next <em>obsession.</em></h1>
396
+ <p>Describe your mood — we'll find the perfect anime.</p>
397
+ <div class="header-line"></div>
398
+ </div>
399
  """)
400
+
401
+ # Step 1
402
+ gr.HTML('<div class="step-card"><div class="step-label"><span class="num">1</span> What are you in the mood for?</div>')
403
+ query_input = gr.Textbox(
404
+ label="", show_label=False, lines=3,
405
+ placeholder='"Something like Attack on Titan but more emotional" or "Cozy slice-of-life with great friendships"'
406
+ )
407
+ gr.HTML("</div>")
408
+
409
+ # Step 2
410
+ gr.HTML('<div class="step-card"><div class="step-label"><span class="num">2</span> Any preferences? <span style="font-weight:300;color:#6a5a4a;margin-left:4px;font-size:10px;">optional</span></div>')
411
  with gr.Row():
412
+ genre_dropdown = gr.Dropdown(
413
+ choices=["None", "Action", "Comedy", "Drama",
414
+ "Romance", "Sci-Fi", "Fantasy", "Thriller"],
415
+ value="None", label="Genre", scale=1
416
+ )
417
+ min_score_slider = gr.Slider(
418
+ minimum=0, maximum=10, value=0, step=0.5, label="Minimum rating (0 = any)", scale=2)
419
+ gr.HTML("</div>")
420
+
421
+ # Step 3
422
+ gr.HTML('<div class="step-card"><div class="step-label"><span class="num">3</span> How many results?</div>')
423
+ n_results_slider = gr.Slider(
424
+ minimum=1, maximum=8, value=3, step=1, label="", show_label=False)
425
+ gr.HTML("</div>")
426
+
427
+ submit_btn = gr.Button(
428
+ "Find my anime →", variant="primary", elem_id="submit-btn")
429
+
430
+ # Loading buffer
431
+ gr.HTML("""
432
+ <div id="ao-loader">
433
+ <div class="ao-loader-phrase">Scanning the anime universe…</div>
434
+ <div class="ao-dots"><span></span><span></span><span></span></div>
435
+ </div>
436
+ """)
437
+
438
+ # Output — elem_id so CSS can target it reliably
439
+ output = gr.Markdown(
440
+ value="*Your recommendations will appear here ✦*",
441
+ elem_id="ao-output-wrap"
442
+ )
443
 
444
  # Examples
445
+ gr.HTML('<div class="examples-label">Try an example</div>')
446
  gr.Examples(
447
  examples=[
448
+ ["Something like Death Note but with a lighter tone", 0, "None", 3],
449
+ ["Romantic comedy with lovable characters", 7.5, "Comedy", 4],
450
+ ["Dark psychological thriller with twists", 8.0, "Thriller", 2],
451
+ ["Epic action with incredible fights", 7.0, "Action", 5],
452
+ ["Wholesome feel-good slice of life", 6.0, "Drama", 3],
453
  ],
454
+ inputs=[query_input, min_score_slider,
455
+ genre_dropdown, n_results_slider],
456
+ label=""
457
  )
458
 
459
+ # Loader JS
460
+ gr.HTML("""
461
+ <script>
462
+ (function() {
463
+ const PHRASES = [
464
+ "Scanning the anime universe\u2026",
465
+ "Consulting the oracle\u2026",
466
+ "Traversing story arcs\u2026",
467
+ "Matching your vibe\u2026",
468
+ "Sifting through thousands of titles\u2026",
469
+ "Almost there\u2026"
470
+ ];
471
+ let phraseTimer = null, phraseIdx = 0;
472
+
473
+ function startLoader() {
474
+ const loader = document.getElementById('ao-loader');
475
+ const wrap = document.getElementById('ao-output-wrap');
476
+ if (loader) loader.style.display = 'block';
477
+ if (wrap) wrap.style.opacity = '0.15';
478
+ phraseIdx = 0;
479
+ const el = document.querySelector('.ao-loader-phrase');
480
+ if (el) el.textContent = PHRASES[0];
481
+ clearInterval(phraseTimer);
482
+ phraseTimer = setInterval(() => {
483
+ phraseIdx = (phraseIdx + 1) % PHRASES.length;
484
+ if (el) {
485
+ el.style.opacity = '0';
486
+ setTimeout(() => { el.textContent = PHRASES[phraseIdx]; el.style.opacity = '1'; }, 300);
487
+ }
488
+ }, 1800);
489
+ }
490
+
491
+ function stopLoader() {
492
+ clearInterval(phraseTimer);
493
+ const loader = document.getElementById('ao-loader');
494
+ const wrap = document.getElementById('ao-output-wrap');
495
+ if (loader) loader.style.display = 'none';
496
+ if (wrap) { wrap.style.transition = 'opacity 0.5s ease'; wrap.style.opacity = '1'; }
497
+ }
498
+
499
+ document.addEventListener('click', function(e) {
500
+ if (e.target && e.target.closest('#submit-btn')) {
501
+ startLoader();
502
+ setTimeout(stopLoader, 25000);
503
+ }
504
+ });
505
+
506
+ function watchOutput() {
507
+ const target = document.getElementById('ao-output-wrap');
508
+ if (!target) { setTimeout(watchOutput, 500); return; }
509
+ new MutationObserver(stopLoader).observe(target, { childList: true, subtree: true, characterData: true });
510
+ }
511
+ watchOutput();
512
+ })();
513
+ </script>
514
+ """)
515
+
516
+ submit_btn.click(fn=get_recommendations, inputs=[
517
+ query_input, min_score_slider, genre_dropdown, n_results_slider], outputs=output)
518
+ query_input.submit(fn=get_recommendations, inputs=[
519
+ query_input, min_score_slider, genre_dropdown, n_results_slider], outputs=output)
520
 
 
521
  if __name__ == "__main__":
522
+ print("Starting Anime Oracle...")
523
+ demo.launch(server_name="0.0.0.0", server_port=7860, share=True)