Skip to content

Commit 970c546

Browse files
authored
Merge branch 'main' into gssoc/bugs1
2 parents 23270d5 + e732c8d commit 970c546

53 files changed

Lines changed: 3085 additions & 772 deletions

Some content is hidden

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

‎.github/workflows/ci.yml‎

Lines changed: 24 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
name: CI – Run Tests
1+
name: CI – Run Tests & Lint
22

33
on:
44
push:
@@ -7,6 +7,26 @@ on:
77
branches: ["main", "master"]
88

99
jobs:
10+
lint:
11+
name: Lint Python
12+
runs-on: ubuntu-latest
13+
steps:
14+
- uses: actions/checkout@v4
15+
- uses: actions/setup-python@v5
16+
with:
17+
python-version: "3.11"
18+
cache: "pip"
19+
- name: Install linters
20+
run: pip install flake8 black
21+
- name: Check code formatting with Black
22+
run: black --check backend tests || true
23+
- name: Lint with flake8
24+
run: |
25+
# Stop the build if there are Python syntax errors or undefined names
26+
flake8 backend tests --count --select=E9,F63,F7,F82 --show-source --statistics
27+
# exit-zero treats all other errors as warnings
28+
flake8 backend tests --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
29+
1030
test:
1131
name: pytest
1232
runs-on: ubuntu-latest
@@ -22,9 +42,10 @@ jobs:
2242
- name: Install dependencies
2343
run: |
2444
pip install --upgrade pip
45+
pip install torch --index-url https://download.pytorch.org/whl/cpu
2546
pip install -r backend/requirements.txt
2647
pip install pytest pytest-cov
27-
pip install flask-wtf bleach Pillow beautifulsoup4 "pydantic[email]" pytest pytest-cov
48+
pip install flask-wtf bleach Pillow beautifulsoup4 "pydantic[email]"
2849
2950
- name: Run pytest
3051
env:
@@ -42,18 +63,4 @@ jobs:
4263
--ignore=tests/test_category_cache.py \
4364
--ignore=tests/test_env_validation.py \
4465
--ignore=tests/test_purchase_links.py \
45-
-v --tb=short --cov=backend
46-
47-
- name: Install dependencies
48-
run: |
49-
pip install --upgrade pip
50-
pip install torch --index-url https://download.pytorch.org/whl/cpu
51-
pip install -r backend/requirements.txt
52-
pip install flask-wtf bleach Pillow pytest pytest-cov
53-
54-
- name: Install dependencies
55-
run: |
56-
pip install --upgrade pip
57-
pip install torch --index-url https://download.pytorch.org/whl/cpu
58-
pip install -r backend/requirements.txt
59-
pip install flask-wtf bleach Pillow beautifulsoup4 pytest pytest-cov
66+
-v --tb=short --cov=backend

‎.gitignore‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ mood_analysis/mood_cache.json
66
*.cache
77

88
# Environment variables
9+
.env
910
*/.env
1011
*/.env.local
1112
*/.env.production

‎README.md‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
> [!WARNING]
2+
> **Netlify builds have stopped** due to limit exceeding as the project was very popular and had regular commits. Changes may not be visible on the deployed site. Please run the app locally.
3+
14
<div align="center">
25

36
# 🌌 BiblioDrift — Drift Through Stories, Not Screens

‎backend/ai_service.py‎

Lines changed: 159 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919

2020
#to generate book spine images dynamically based on title and author
2121

22-
from backend.spine_generator import create_spine
22+
from spine_generator import create_spine
2323

2424
def process_new_book(book_data):
2525
# 1. Save book to your database first
@@ -508,6 +508,113 @@ def _build_flat_prompt(system: str, msgs: list) -> str:
508508
logger.error(f"generate_chat failed: {type(e).__name__}: {e}", exc_info=True)
509509

510510
return None
511+
512+
def generate_chat_stream(self, system_prompt: str, messages: list, max_tokens: Optional[int] = None):
513+
"""Yield chunks of text from the LLM for streaming."""
514+
if not self.is_available():
515+
yield "The candles are flickering and my connection to the ether seems troubled today. "
516+
return
517+
518+
if max_tokens is None:
519+
max_tokens = self.config.get('gemini_max_tokens', 600)
520+
521+
# Per-model context window limits (conservative estimates)
522+
_MODEL_CONTEXT_LIMITS = {
523+
'llama-3.1-8b-instant': 8192,
524+
'llama-3.3-70b-versatile': 32768,
525+
'gpt-3.5-turbo': 4096,
526+
'gpt-4': 8192,
527+
'gpt-4o': 16384,
528+
'models/gemini-2.0-flash-lite': 32768,
529+
'models/gemini-1.5-flash': 32768,
530+
}
531+
532+
def _get_context_limit(model_name: str) -> int:
533+
return _MODEL_CONTEXT_LIMITS.get(model_name, 4096)
534+
535+
try:
536+
# --- Gemini ---
537+
if self.gemini_client and (self.preferred_llm == 'gemini' or not self.groq_client):
538+
try:
539+
from google.genai import types
540+
context_limit = _get_context_limit(self.config['gemini_model'])
541+
trimmed = self.trim_history_to_token_budget(
542+
system_prompt, messages, max_tokens, context_limit
543+
)
544+
545+
flat_prompt = system_prompt + "\n\n"
546+
for m in trimmed:
547+
role = "Elara" if m.get("role") == "assistant" else "Customer"
548+
flat_prompt += f"{role}: {m.get('content', '')}\n"
549+
flat_prompt += "Elara:"
550+
551+
response = self.gemini_client.models.generate_content_stream(
552+
model=self.config['gemini_model'],
553+
contents=flat_prompt,
554+
)
555+
for chunk in response:
556+
if chunk.text:
557+
yield chunk.text
558+
return
559+
except Exception as e:
560+
logger.warning(f"Gemini streaming chat failed, falling back: {e}")
561+
562+
# --- Groq ---
563+
if self.groq_client:
564+
try:
565+
context_limit = _get_context_limit(self.config['groq_model'])
566+
trimmed = self.trim_history_to_token_budget(
567+
system_prompt, messages, max_tokens, context_limit
568+
)
569+
570+
groq_messages = [{"role": "system", "content": system_prompt}] + [
571+
{"role": m.get("role", "user"), "content": m.get("content", "")}
572+
for m in trimmed
573+
]
574+
response = self.groq_client.chat.completions.create(
575+
model=self.config['groq_model'],
576+
messages=groq_messages,
577+
max_tokens=min(max_tokens, self.config['groq_max_tokens']),
578+
temperature=self.config['groq_temperature'],
579+
stream=True
580+
)
581+
for chunk in response:
582+
content = chunk.choices[0].delta.content
583+
if content:
584+
yield content
585+
return
586+
except Exception as e:
587+
logger.warning(f"Groq streaming chat failed, falling back: {e}")
588+
589+
# --- OpenAI ---
590+
if self.openai_client:
591+
from openai import OpenAI
592+
client = OpenAI(api_key=os.getenv('OPENAI_API_KEY'))
593+
context_limit = _get_context_limit(self.config['openai_model'])
594+
trimmed = self.trim_history_to_token_budget(
595+
system_prompt, messages, max_tokens, context_limit
596+
)
597+
598+
oai_messages = [{"role": "system", "content": system_prompt}] + [
599+
{"role": m.get("role", "user"), "content": m.get("content", "")}
600+
for m in trimmed
601+
]
602+
response = client.chat.completions.create(
603+
model=self.config['openai_model'],
604+
messages=oai_messages,
605+
max_tokens=min(max_tokens, self.config['openai_max_tokens']),
606+
temperature=self.config['openai_temperature'],
607+
stream=True
608+
)
609+
for chunk in response:
610+
content = chunk.choices[0].delta.content
611+
if content:
612+
yield content
613+
return
614+
615+
except Exception as e:
616+
logger.error(f"generate_chat_stream failed: {type(e).__name__}: {e}", exc_info=True)
617+
yield "An error occurred while peering into the books. Please ask again."
511618

512619
def generate_text(self, prompt: str, max_tokens: Optional[int] = None, retry_count: int = 0) -> Optional[str]:
513620
"""Generate text using available LLM service with retry logic."""
@@ -653,7 +760,7 @@ def _generate_with_gemini(self, prompt: str, max_tokens: int) -> Optional[str]:
653760
llm_service = LLMService()
654761

655762
__all__ = ['generate_book_note', 'get_ai_recommendations', 'get_category_books',
656-
'get_book_mood_tags_safe', 'generate_chat_response', 'llm_service',
763+
'get_book_mood_tags_safe', 'generate_chat_response', 'generate_chat_response_stream', 'llm_service',
657764
'LLMService', 'PromptTemplates']
658765

659766

@@ -963,6 +1070,56 @@ def generate_chat_response(user_message: str, conversation_history: list = []) -
9631070
import random
9641071
return random.choice(variations)
9651072

1073+
def generate_chat_response_stream(user_message: str, conversation_history: list = []):
1074+
"""
1075+
Yield an emotionally rich, persona-driven chat response from Elara.
1076+
"""
1077+
if not llm_service.is_available():
1078+
logger.warning("generate_chat_response_stream: No LLM available")
1079+
yield "The candles are flickering and my connection to the ether seems troubled today. "
1080+
yield "Come back in a moment — the books are waiting, and so am I."
1081+
return
1082+
1083+
messages = []
1084+
for msg in (conversation_history or []):
1085+
role = msg.get("type", msg.get("role", "user"))
1086+
if role in ("bookseller", "assistant"):
1087+
role = "assistant"
1088+
else:
1089+
role = "user"
1090+
content = msg.get("content", "")
1091+
if content:
1092+
messages.append({"role": role, "content": content})
1093+
1094+
messages.append({"role": "user", "content": user_message})
1095+
1096+
yield from llm_service.generate_chat_stream(
1097+
system_prompt=_WISE_BOOKSELLER_SYSTEM_PROMPT,
1098+
messages=messages,
1099+
)
1100+
1101+
# SMART FALLBACK: A variety of poetic responses that adapt to keywords
1102+
msg_lower = user_message.lower()
1103+
1104+
if any(k in msg_lower for k in ['rain', 'melancholy', 'sad', 'quiet']):
1105+
yield "The rain has a way of turning the heart into a library of its own. I've gathered these quiet, thoughtful volumes for your pensive mood."
1106+
elif any(k in msg_lower for k in ['adventure', 'journey', 'travel', 'exciting']):
1107+
yield "Ah, a soul that yearns for the horizon! The dust on these covers is from distant worlds... here are a few maps for your next great journey."
1108+
elif any(k in msg_lower for k in ['cozy', 'warm', 'happy', 'gentle']):
1109+
yield "There is a particular warmth in finding the right story at the right time. Let me tuck these gentle tales into your shelf for a comfortable evening."
1110+
elif any(k in msg_lower for k in ['dark', 'mystery', 'thriller', 'shadow']):
1111+
yield "Some stories prefer the shadows, whispering truths we only dare to hear at night. I've pulled these mysterious tomes from the back shelf for you."
1112+
else:
1113+
# Generic but varied fallbacks
1114+
variations = [
1115+
"The books have been whispering your name today. I've pulled a few that seem particularly eager to meet you.",
1116+
"Every reader is a traveler, and every book a destination. Which of these paths shall we walk today?",
1117+
"I've spent a lifetime listening to the scent of old paper... and it tells me these stories belong in your hands.",
1118+
"The stars and the ink seem to be in alignment. Here is what I've found in the quiet corners of the shop for you."
1119+
]
1120+
import random
1121+
yield random.choice(variations)
1122+
9661123
# =========================================================================
9671124
# VIBE-CHECK ENDPOINT LOGIC
9681125
# Implements strict validation and caching per PR #903 feedback.

0 commit comments

Comments
 (0)