|
|
import streamlit as st |
|
|
import os |
|
|
import uuid |
|
|
from streamlit_chat import message |
|
|
|
|
|
|
|
|
from langchain_huggingface import HuggingFaceEndpoint, ChatHuggingFace |
|
|
|
|
|
|
|
|
from langchain_core.messages import ( |
|
|
AIMessage, |
|
|
HumanMessage, |
|
|
SystemMessage, |
|
|
BaseMessage |
|
|
) |
|
|
|
|
|
|
|
|
HF_API_TOKEN = os.environ.get("HUGGINGFACEHUB_API_TOKEN") |
|
|
|
|
|
|
|
|
SYSTEM_PROMPT = SystemMessage( |
|
|
content=( |
|
|
"You are Kitchen Buddy 👨🍳, a warm and friendly culinary assistant. " |
|
|
"Your job is to help people with anything related to food, cooking, or cuisine.\n\n" |
|
|
"You can:\n" |
|
|
"- Explain what ingredients are, their uses, and their cultural background\n" |
|
|
"- Suggest recipes and meal ideas\n" |
|
|
"- Offer ingredient substitutions\n" |
|
|
"- Teach cooking techniques and science\n" |
|
|
"- Provide healthy diet adaptations\n" |
|
|
"- Explore global cuisines & traditions\n\n" |
|
|
"Keep your tone helpful and approachable. " |
|
|
"If a user asks about a food item (e.g., 'what are apples'), explain what it is and how it’s commonly used. " |
|
|
"If they ask what to cook with it, suggest a few recipes. " |
|
|
"If something is unrelated to food or cooking, politely redirect back to culinary topics." |
|
|
) |
|
|
) |
|
|
|
|
|
|
|
|
st.set_page_config(page_title="Kitchen Buddy 👨🍳", layout="centered") |
|
|
st.title("👨🍳 Kitchen Buddy") |
|
|
|
|
|
|
|
|
available_models = { |
|
|
"Mistral-7B-Instruct-v0.2": "Mistralai/Mistral-7B-Instruct-v0.2", |
|
|
"Llama-2-7B-Chat": "meta-llama/Llama-2-7b-chat-hf", |
|
|
"Qwen1.5-7B-Chat": "Qwen/Qwen1.5-7B-Chat", |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
if 'selected_model_key' not in st.session_state: |
|
|
st.session_state.selected_model_key = "Mistral-7B-Instruct-v0.2" |
|
|
|
|
|
|
|
|
@st.cache_resource |
|
|
def initialize_llm(repo_id): |
|
|
if not HF_API_TOKEN: |
|
|
return None |
|
|
|
|
|
os.environ['HUGGINGFACEHUB_API_TOKEN'] = HF_API_TOKEN |
|
|
try: |
|
|
llm = HuggingFaceEndpoint( |
|
|
repo_id=repo_id, |
|
|
task="text-generation", |
|
|
max_new_tokens=512, |
|
|
temperature=0.7, |
|
|
do_sample=True, |
|
|
repetition_penalty=1.03 |
|
|
) |
|
|
chat_model = ChatHuggingFace(llm=llm) |
|
|
return chat_model |
|
|
except Exception as e: |
|
|
st.error(f"❌ Failed to initialize {repo_id}. Check API key and model availability.") |
|
|
print(f"Detailed LLM init error for {repo_id}: {e}") |
|
|
return None |
|
|
|
|
|
def get_current_repo_id(): |
|
|
return available_models.get(st.session_state.selected_model_key, available_models["Mistral-7B-Instruct-v0.2"]) |
|
|
|
|
|
|
|
|
CHAT_MODEL = initialize_llm(get_current_repo_id()) |
|
|
|
|
|
|
|
|
current_model_display = st.session_state.selected_model_key |
|
|
st.markdown(f""" |
|
|
Your friendly culinary assistant — ask about recipes, ingredients, and cooking techniques. |
|
|
**🤖 Model in use:** `{current_model_display}` |
|
|
""") |
|
|
|
|
|
|
|
|
if CHAT_MODEL is None: |
|
|
st.warning("⚠️ Model initialization failed. Please check your HF API token and try reloading.") |
|
|
|
|
|
|
|
|
def new_chat(): |
|
|
new_id = str(uuid.uuid4()) |
|
|
st.session_state.chats[new_id] = [SYSTEM_PROMPT] |
|
|
st.session_state.chat_titles[new_id] = "New Chat" |
|
|
st.session_state.current_chat_id = new_id |
|
|
|
|
|
if 'chats' not in st.session_state: |
|
|
st.session_state.chats = {} |
|
|
st.session_state.chat_titles = {} |
|
|
new_chat() |
|
|
if 'current_chat_id' not in st.session_state: |
|
|
new_chat() |
|
|
if 'generate_next' not in st.session_state: |
|
|
st.session_state.generate_next = False |
|
|
|
|
|
|
|
|
def get_current_messages() -> list[BaseMessage]: |
|
|
return st.session_state.chats.get(st.session_state.current_chat_id, [SYSTEM_PROMPT]) |
|
|
|
|
|
def set_current_chat(chat_id): |
|
|
st.session_state.current_chat_id = chat_id |
|
|
|
|
|
def convert_to_streamlit_message(msg: BaseMessage): |
|
|
if isinstance(msg, SystemMessage): |
|
|
return None, None |
|
|
role = "user" if isinstance(msg, HumanMessage) else "assistant" |
|
|
return msg.content, role |
|
|
|
|
|
|
|
|
|
|
|
with st.sidebar: |
|
|
|
|
|
st.subheader("🤖 Model Selector") |
|
|
selected_key = st.selectbox( |
|
|
"Choose a model:", |
|
|
options=list(available_models.keys()), |
|
|
index=list(available_models.keys()).index(st.session_state.selected_model_key), |
|
|
key="model_selector" |
|
|
) |
|
|
if selected_key != st.session_state.selected_model_key: |
|
|
st.session_state.selected_model_key = selected_key |
|
|
|
|
|
st.cache_resource.clear() |
|
|
st.success(f"✅ Switched to {selected_key}. Reloading model...") |
|
|
st.rerun() |
|
|
|
|
|
if st.button("🔄 Reload Current Model", use_container_width=True): |
|
|
st.cache_resource.clear() |
|
|
st.success("✅ Reloading model...") |
|
|
st.rerun() |
|
|
|
|
|
|
|
|
has_real_chats = any( |
|
|
len(history) > 1 for history in st.session_state.chats.values() |
|
|
) |
|
|
|
|
|
if not has_real_chats: |
|
|
|
|
|
st.button("📭 No saved conversations yet", use_container_width=True, disabled=True) |
|
|
|
|
|
st.markdown(""" |
|
|
### 👨🍳 Welcome! |
|
|
Ask me anything about cooking: |
|
|
- Recipes and ideas |
|
|
- Ingredient substitutions |
|
|
- Cooking techniques |
|
|
*Try asking:* |
|
|
• "What can I make with apples?" |
|
|
• "How do I cook pasta al dente?" |
|
|
""") |
|
|
else: |
|
|
|
|
|
if st.button("🟥 New Chat", use_container_width=True): |
|
|
new_chat() |
|
|
st.rerun() |
|
|
|
|
|
st.markdown("---") |
|
|
st.subheader("📜 Chat History") |
|
|
|
|
|
|
|
|
for chat_id, title in list(st.session_state.chat_titles.items()): |
|
|
if len(st.session_state.chats.get(chat_id, [SYSTEM_PROMPT])) > 1: |
|
|
display_title = title |
|
|
is_current = chat_id == st.session_state.current_chat_id |
|
|
|
|
|
if st.button( |
|
|
display_title, |
|
|
key=f"chat_switch_{chat_id}", |
|
|
type="primary" if is_current else "secondary", |
|
|
use_container_width=True |
|
|
): |
|
|
set_current_chat(chat_id) |
|
|
st.rerun() |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if prompt := st.chat_input("Ask about a recipe, technique, or substitution..."): |
|
|
if CHAT_MODEL is None: |
|
|
st.session_state.chats[st.session_state.current_chat_id].append(HumanMessage(content=prompt)) |
|
|
st.session_state.chats[st.session_state.current_chat_id].append( |
|
|
AIMessage(content="Error: Model is not initialized. Check API key setup.") |
|
|
) |
|
|
st.rerun() |
|
|
|
|
|
|
|
|
st.session_state.chats[st.session_state.current_chat_id].append(HumanMessage(content=prompt)) |
|
|
|
|
|
|
|
|
|
|
|
culinary_keywords = [ |
|
|
|
|
|
"cook", "cooking", "kitchen", "chef", "meal", "food", "dish", "recipe", "cuisine", "menu", "flavor", "taste", |
|
|
|
|
|
"ingredient", "spice", "herb", "oil", "salt", "pepper", "garlic", "onion", "tomato", "butter", "cheese", |
|
|
"meat", "beef", "pork", "chicken", "lamb", "fish", "seafood", "shrimp", "crab", "lobster", |
|
|
"vegetable", "fruit", "grain", "rice", "pasta", "bread", "noodles", "beans", "tofu", "egg", |
|
|
|
|
|
"bake", "roast", "grill", "barbecue", "bbq", "fry", "deep fry", "saute", "sauté", "boil", "steam", "poach", |
|
|
"simmer", "stew", "braise", "marinate", "blend", "chop", "slice", "dice", "whisk", "knead", "ferment", |
|
|
|
|
|
"soup", "salad", "sandwich", "burger", "pizza", "pasta", "stew", "curry", "sauce", "stir fry", "omelette", |
|
|
"dessert", "cake", "cookie", "pie", "pastry", "bread", "tart", "pudding", "ice cream", |
|
|
|
|
|
"italian", "french", "spanish", "greek", "mediterranean", "japanese", "chinese", "korean", "thai", |
|
|
"vietnamese", "indian", "mexican", "latin", "filipino", "turkish", "middle eastern", "moroccan", |
|
|
|
|
|
"vegan", "vegetarian", "gluten-free", "keto", "paleo", "halal", "kosher", "low-carb", "low-fat", |
|
|
|
|
|
"coffee", "tea", "smoothie", "wine", "cocktail", "beer", "drink", "juice", |
|
|
|
|
|
"thanksgiving", "christmas", "new year", "ramadan", "eid", "hanukkah", "valentine", "birthday", "party", |
|
|
|
|
|
"sous vide", "confit", "smoking", "curing", "pickling", "plating", "molecular gastronomy", |
|
|
|
|
|
"mise en place", "umami", "maillard reaction", "deglaçage", "roux", "stock", "broth", |
|
|
|
|
|
"truffle", "saffron", "caviar", "foie gras", "kimchi", "kombu", "nori", "tamarind", "matcha", "miso", |
|
|
|
|
|
"diabetic-friendly", "heart-healthy", "organic", "sustainable", "farm-to-table", |
|
|
|
|
|
"blender", "mixer", "pressure cooker", "air fryer", "cast iron", "oven", "microwave", "thermometer" |
|
|
] |
|
|
|
|
|
|
|
|
|
|
|
culinary_phrases = [ |
|
|
"what can i make with", |
|
|
"how do i cook", |
|
|
"how to cook", |
|
|
"how to make", |
|
|
"substitute for", |
|
|
"what is", |
|
|
"uses of" |
|
|
] |
|
|
|
|
|
prompt_lower = prompt.lower() |
|
|
|
|
|
|
|
|
is_culinary = ( |
|
|
any(word in prompt_lower for word in culinary_keywords) or |
|
|
any(phrase in prompt_lower for phrase in culinary_phrases) |
|
|
) |
|
|
|
|
|
if not is_culinary: |
|
|
restriction_msg = AIMessage(content="⚠️ I can only answer questions about cooking, recipes, ingredients, or culinary techniques. Please ask something food-related.") |
|
|
st.session_state.chats[st.session_state.current_chat_id].append(restriction_msg) |
|
|
st.rerun() |
|
|
else: |
|
|
st.session_state.generate_next = True |
|
|
st.rerun() |
|
|
|
|
|
|
|
|
messages = get_current_messages() |
|
|
|
|
|
|
|
|
if len(messages) == 1 and isinstance(messages[0], SystemMessage): |
|
|
message("Start the conversation by typing your first culinary question below!", key="welcome_bubble") |
|
|
|
|
|
for i in range(1, len(messages)): |
|
|
content, role = convert_to_streamlit_message(messages[i]) |
|
|
if not content: |
|
|
continue |
|
|
|
|
|
if role == "assistant": |
|
|
if "recipe" in content.lower(): |
|
|
message(f"👨🍳 **Chef’s Recipe:**\n\n{content}", key=f"chat_ai_{i}") |
|
|
else: |
|
|
message(f"👨🍳 {content}", key=f"chat_ai_{i}") |
|
|
elif role == "user": |
|
|
message(content, is_user=True, key=f"chat_user_{i}") |
|
|
|
|
|
|
|
|
|
|
|
if st.session_state.generate_next: |
|
|
st.session_state.generate_next = False |
|
|
|
|
|
full_history = get_current_messages() |
|
|
|
|
|
|
|
|
with st.spinner("👨🍳 Our culinary expert is crafting your response..."): |
|
|
try: |
|
|
ai_message: AIMessage = CHAT_MODEL.invoke(full_history) |
|
|
st.session_state.chats[st.session_state.current_chat_id].append(ai_message) |
|
|
|
|
|
|
|
|
if st.session_state.chat_titles[st.session_state.current_chat_id] == "New Chat": |
|
|
st.session_state.chat_titles[st.session_state.current_chat_id] = full_history[-1].content[:30] + "..." |
|
|
except Exception as e: |
|
|
error_message = "I'm sorry, I encountered a brief issue while preparing the answer. Please try again." |
|
|
st.session_state.chats[st.session_state.current_chat_id].append(AIMessage(content=error_message)) |
|
|
print(f"Full LLM invocation error: {e}") |
|
|
|
|
|
st.rerun() |