Skip to content

Commit 7e2c3bd

Browse files
Merge main into pr-950
2 parents b14fa58 + ae045f8 commit 7e2c3bd

88 files changed

Lines changed: 11629 additions & 6061 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.gitignore‎

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,10 @@ mood_analysis/mood_cache.json
66
*.cache
77

88
# Environment variables
9-
/.env
10-
/.env.local
11-
/.env.production
12-
/.env.development
9+
*/.env
10+
*/.env.local
11+
*/.env.production
12+
*/.env.development
1313

1414
# Sensitive local files
1515
*.token
@@ -32,4 +32,9 @@ instance/
3232
*.db
3333
# Virtual Environment
3434
venv/
35+
36+
node_modules/
3537
.venv/
38+
39+
# Local untracked scripts folder
40+
untracked/

‎Dockerfile‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ FROM python:3.11-slim
44
# Set environment variables
55
ENV PYTHONDONTWRITEBYTECODE=1
66
ENV PYTHONUNBUFFERED=1
7-
ENV FLASK_APP=app.py
7+
ENV FLASK_APP=backend/app.py
88
ENV FLASK_HOST=0.0.0.0
99
ENV PORT=5000
1010

@@ -28,4 +28,4 @@ COPY . .
2828
EXPOSE 5000
2929

3030
# Run the application
31-
CMD ["python", "app.py"]
31+
CMD ["python", "backend/app.py"]

‎Dockerfile.frontend‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
FROM nginx:alpine
2+
COPY ./nginx.conf /etc/nginx/conf.d/default.conf
3+
COPY ./frontend /usr/share/nginx/html
4+
EXPOSE 80
5+
CMD ["nginx", "-g", "daemon off;"]

‎README.md‎

Lines changed: 44 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,9 @@ where stories are explored through <b>mood, atmosphere, and feeling</b> instead
2020
🌧️ Mood-first discovery • 📚 Immersive reading • 🤖 AI-powered storytelling
2121
</p>
2222

23-
24-
[🚀 Live Demo](https://bibliodrift-dm.netlify.app/) (static frontend; AI/auth need a deployed backend — see [docs/contributing.md](docs/contributing.md)) •
25-
[📚 Documentation](docs/) •
26-
[🤝 Contributing](docs/contributing.md)
27-
23+
<a href="https://www.netlify.com">
24+
<img src="https://www.netlify.com/assets/badges/netlify-badge-dark.svg" alt="Deploys by Netlify" />
25+
</a>
2826
</div>
2927

3028
---
@@ -105,7 +103,48 @@ It feels like:
105103
| Storage | LocalStorage |
106104

107105
---
106+
## 🚀 Backend Deployment & BACKEND_URL Setup
107+
108+
> ⚠️ The frontend is deployed on Netlify, but the Flask backend is **not yet deployed**.
109+
> Google Sign-In and all AI features require the backend to be running.
110+
111+
### Why is this needed?
112+
When a user clicks "Sign in with Google", the browser sends a request to `/api/v1/auth/google`.
113+
Netlify needs to know where to forward that request, that's what `BACKEND_URL` is for.
114+
Without it, Netlify has no rule for `/api/v1/*` and shows a 404 error.
115+
116+
### Step 1 — Deploy the Flask backend
117+
118+
You can deploy the backend (located in the `/backend` folder) to either:
119+
120+
| Platform | Free Tier | Docs |
121+
|---|---|---|
122+
| [Render](https://render.com) | ✅ Yes | [Render Python Docs](https://render.com/docs/deploy-flask) |
123+
| [Railway](https://railway.app) | ✅ Yes | [Railway Docs](https://docs.railway.app) |
108124

125+
After deploying, you'll get a public URL like:
126+
https://your-app.onrender.com
127+
128+
### Step 2 — Set BACKEND_URL in Netlify
129+
130+
1. Go to [Netlify Dashboard](https://app.netlify.com)
131+
2. Open your site → **Site Configuration → Environment Variables**
132+
3. Click **Add a variable**
133+
4. Set:
134+
- **Key:** `BACKEND_URL`
135+
- **Value:** `https://your-app.onrender.com` *(no trailing slash)*
136+
5. Click **Save**
137+
138+
### Step 3 — Redeploy the Netlify site
139+
140+
After setting the variable:
141+
1. Go to **Deploys** tab in your Netlify dashboard
142+
2. Click **Trigger deploy → Deploy site**
143+
3. This reruns `build_netlify.py`, which picks up `BACKEND_URL` and adds the proxy rule to `_redirects`
144+
145+
After this, "Sign in with Google" will correctly reach the Flask backend. ✅
146+
147+
---
109148
## 🧠 System Architecture
110149

111150
```mermaid

‎add_pwa_css.py‎

Lines changed: 0 additions & 15 deletions
This file was deleted.

‎add_theme_css.py‎

Lines changed: 0 additions & 37 deletions
This file was deleted.

‎backend/ai_service.py‎

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# AI service logic with LLM integration (OpenAI/Gemini)
22
# Implements 'generate_book_note' and 'get_ai_recommendations'. All recommendations MUST be AI-based.
3+
34
# Enhanced with comprehensive caching for expensive operations
45

56

@@ -219,6 +220,25 @@ def get_category_books_prompt(category: str, vibe_description: str, count: int =
219220
- Output the JSON array only.
220221
"""
221222

223+
@staticmethod
224+
def get_vibe_check_prompt(vibe_prompt: str, count: int = 3) -> str:
225+
"""
226+
Prompt for generating hyper-personalized vibe-based book recommendations.
227+
"""
228+
return f"""You are Elara, an emotionally intuitive bookseller.
229+
A reader is looking for books matching this exact hyper-specific vibe or mood: "{vibe_prompt}"
230+
231+
Return exactly {count} real, verifiable books that perfectly capture this energy.
232+
Output ONLY a valid JSON array. No markdown fences. No text before or after.
233+
Schema:
234+
[
235+
{{
236+
"title": "Exact book title",
237+
"author": "Author full name",
238+
"reason": "One brilliant, atmospheric sentence explaining why it fits the requested vibe perfectly."
239+
}}
240+
]
241+
"""
222242

223243
class LLMService:
224244
"""
@@ -779,6 +799,46 @@ def get_category_books(category: str, vibe_description: str, count: int = 5) ->
779799
logger.info("get_category_books: %d books returned for '%s'", len(valid_books), category)
780800
return valid_books
781801

802+
def get_vibe_recommendations(vibe_prompt: str, count: int = 3) -> list:
803+
"""
804+
Generate a list of real books based on a highly specific natural-language vibe.
805+
Returns a list of dicts: [{"title": ..., "author": ..., "reason": ...}, ...]
806+
"""
807+
if not llm_service.is_available():
808+
logger.warning("get_vibe_recommendations: no LLM configured")
809+
return []
810+
811+
prompt = PromptTemplates.get_vibe_check_prompt(vibe_prompt, count)
812+
813+
# Utilizing the existing LLMService pipeline
814+
raw = llm_service.generate_text(
815+
prompt,
816+
max_tokens=llm_service.config.get('category_books_max_tokens', 600),
817+
)
818+
819+
if not raw:
820+
logger.error("get_vibe_recommendations: LLM returned None")
821+
return []
822+
823+
# Use the file's existing robust JSON extractor
824+
parsed = _extract_json(raw)
825+
826+
if not isinstance(parsed, list):
827+
logger.error("get_vibe_recommendations: expected JSON array")
828+
return []
829+
830+
# Validate and clean the output
831+
valid_books = []
832+
for item in parsed:
833+
if isinstance(item, dict) and "title" in item and "author" in item:
834+
valid_books.append({
835+
"title": item["title"],
836+
"author": item["author"],
837+
"reason": item.get("reason", "This perfectly matches your requested vibe."),
838+
})
839+
840+
logger.info("get_vibe_recommendations: %d books returned for vibe '%s'", len(valid_books), vibe_prompt)
841+
return valid_books
782842

783843
@cache_mood_tags
784844
def get_book_mood_tags_safe(title: str, author: str = "") -> list:
@@ -902,3 +962,96 @@ def generate_chat_response(user_message: str, conversation_history: list = []) -
902962
]
903963
import random
904964
return random.choice(variations)
965+
966+
# =========================================================================
967+
# VIBE-CHECK ENDPOINT LOGIC
968+
# Implements strict validation and caching per PR #903 feedback.
969+
# =========================================================================
970+
971+
import hashlib
972+
from cache_service import cache_service
973+
974+
def generate_vibe_check(raw_vibe_prompt: str, raw_count: int = 5) -> Optional[dict]:
975+
"""
976+
Generate an AI vibe check with strict server-side validation and caching.
977+
978+
Args:
979+
raw_vibe_prompt: The raw emotional/vibe query from the user.
980+
raw_count: The requested number of book recommendations.
981+
982+
Returns:
983+
Dict containing the AI response and recommendations, or None on failure.
984+
"""
985+
986+
# ---------------------------------------------------------
987+
# 1. VALIDATION: Clamp the Count (Security & Cost Control)
988+
# ---------------------------------------------------------
989+
try:
990+
count = int(raw_count)
991+
if count < 1:
992+
count = 1
993+
elif count > 10: # Hard limit to prevent API abuse
994+
count = 10
995+
except (ValueError, TypeError):
996+
count = 5 # Safe default
997+
998+
# ---------------------------------------------------------
999+
# 2. VALIDATION: Vibe Prompt Length (Prompt Injection Prevention)
1000+
# ---------------------------------------------------------
1001+
if not raw_vibe_prompt or not isinstance(raw_vibe_prompt, str):
1002+
logger.warning("generate_vibe_check: Empty or invalid prompt received.")
1003+
return None
1004+
1005+
safe_vibe_prompt = raw_vibe_prompt.strip()
1006+
1007+
# Restrict prompt to 400 characters to prevent huge payload bills
1008+
if len(safe_vibe_prompt) > 400:
1009+
logger.warning(f"generate_vibe_check: Prompt truncated from {len(safe_vibe_prompt)} chars.")
1010+
safe_vibe_prompt = safe_vibe_prompt[:400]
1011+
1012+
# ---------------------------------------------------------
1013+
# 3. CACHING: Prevent Expensive Redundant LLM Calls
1014+
# ---------------------------------------------------------
1015+
# Create a deterministic hash of the sanitized inputs
1016+
prompt_hash = hashlib.md5(f"{safe_vibe_prompt}_{count}".encode('utf-8')).hexdigest()
1017+
cache_key = f"vibe_check_{prompt_hash}"
1018+
1019+
cached_result = cache_service.get(cache_key)
1020+
if cached_result:
1021+
logger.info(f"generate_vibe_check: Serving cached response for key {cache_key}")
1022+
return cached_result
1023+
1024+
# ---------------------------------------------------------
1025+
# 4. LLM GENERATION
1026+
# ---------------------------------------------------------
1027+
if not llm_service.is_available():
1028+
logger.error("generate_vibe_check: No LLM service available.")
1029+
return None
1030+
1031+
# Construct a specific prompt for the vibe check
1032+
system_prompt = (
1033+
f"You are a literary vibe-checker. The user has described their current mood: '{safe_vibe_prompt}'. "
1034+
f"Give them a short, poetic reading diagnosis (under 50 words) and recommend exactly {count} books "
1035+
"that perfectly match this specific emotional frequency. Format your response clearly."
1036+
)
1037+
1038+
try:
1039+
response_text = llm_service.generate_text(
1040+
prompt=system_prompt,
1041+
max_tokens=llm_service.config.get('recommendation_max_tokens', 250)
1042+
)
1043+
1044+
if response_text:
1045+
result = {
1046+
"vibe_diagnosis": response_text,
1047+
"sanitized_query": safe_vibe_prompt,
1048+
"count_returned": count
1049+
}
1050+
1051+
# Cache the successful result for 24 hours
1052+
cache_service.set(cache_key, result, timeout=86400)
1053+
return result
1054+
1055+
except Exception as e:
1056+
logger.error(f"generate_vibe_check failed: {str(e)}", exc_info=True)
1057+
return None

0 commit comments

Comments
 (0)