streamlit-chatbot / src /streamlit_app.py
akhaliq's picture
akhaliq HF Staff
Upload src/streamlit_app.py with huggingface_hub
f250a9c verified
Raw
History Blame Contribute Delete
6.88 kB
import ast
import operator as op
import random
from datetime import datetime
import json
import re
import streamlit as st
# -----------------------------
# Utilities
# -----------------------------
def safe_eval(expr: str):
"""
Safely evaluate a simple arithmetic expression using Python's AST.
Supports: +, -, *, /, %, //, **, parentheses, unary +/-, ints/floats.
"""
# Map AST operator nodes to actual Python operators
operators = {
ast.Add: op.add,
ast.Sub: op.sub,
ast.Mult: op.mul,
ast.Div: op.truediv,
ast.Mod: op.mod,
ast.FloorDiv: op.floordiv,
ast.Pow: op.pow,
ast.UAdd: op.pos,
ast.USub: op.neg,
}
def _eval(node):
if isinstance(node, ast.Expression):
return _eval(node.body)
if isinstance(node, ast.Constant) and isinstance(node.value, (int, float)):
return node.value
if isinstance(node, ast.BinOp):
if type(node.op) not in operators:
raise ValueError("Unsupported operator.")
return operators[type(node.op)](_eval(node.left), _eval(node.right))
if isinstance(node, ast.UnaryOp) and type(node.op) in (ast.UAdd, ast.USub):
return operators[type(node.op)](_eval(node.operand))
if isinstance(node, ast.Paren):
return _eval(node.value)
raise ValueError("Unsupported expression.")
# Allow only a strict subset of characters
if not re.fullmatch(r"[0-9\.\+\-\*\/%\(\)\s]+", expr):
raise ValueError("Expression contains invalid characters.")
try:
parsed = ast.parse(expr, mode="eval")
return _eval(parsed)
except Exception as e:
raise ValueError(str(e)) from e
def small_talk_response(text: str) -> str:
"""Very simple keyword-based small talk."""
low = text.lower()
if any(w in low for w in ["hi", "hello", "hey"]):
return "Hello! How can I help you today?"
if "time" in low:
return f"The current time is {datetime.now().strftime('%H:%M:%S')}."
if "date" in low or "day" in low:
return f"Today is {datetime.now().strftime('%A, %B %d, %Y')}."
if "joke" in low:
jokes = [
"Why do programmers prefer dark mode? Because light attracts bugs.",
"I told my computer I needed a break, and it said 'No problem, I'll go to sleep.'",
"There are only 10 types of people in the world: those who understand binary and those who don’t.",
]
return random.choice(jokes)
if "weather" in low:
return "I can't check live weather here, but I hope it's nice where you are!"
if "name" in low:
return "I'm Streamlit SimpleBot. Nice to meet you!"
return (
"I'm a simple small-talk bot. You can ask me for the time, a joke, my name, "
"or switch to Calculator mode for math."
)
def friendly_helper(text: str) -> str:
"""A very simple friendly helper with a few smarts."""
low = text.lower().strip()
# Try to detect plain math even in Friendly mode if it's clearly an expression
if re.fullmatch(r"[0-9\.\+\-\*\/%\(\)\s]+", text):
try:
result = safe_eval(text)
return f"Here's the result: {result}"
except Exception:
pass
# Simple tips
if low.endswith("?"):
return (
"That's a great question! I'm a lightweight local bot, so I can handle small talk and simple math. "
"Try asking for the time, a joke, or enter a math expression like (2+3)*4."
)
# If user asks for help
if "help" in low:
return "I can chat casually and do quick calculations. Switch modes from the sidebar to explore."
# Default friendly echo with a light rephrase
return f"I hear you: '{text}'. How would you like me to help with that?"
def calculator_response(text: str) -> str:
"""Use safe_eval to compute the result or provide guidance."""
try:
result = safe_eval(text)
return f"{result}"
except Exception as e:
return (
"I can calculate expressions with +, -, *, /, %, //, ** and parentheses.\n"
"Example: (2 + 3) * 4\n"
f"Error: {e}"
)
def generate_reply(user_text: str, mode: str) -> str:
"""Route user input to the selected bot mode."""
if mode == "Echo":
return f"You said: {user_text}"
if mode == "Calculator":
return calculator_response(user_text)
if mode == "Small talk":
return small_talk_response(user_text)
# Default: Friendly helper
return friendly_helper(user_text)
def init_chat(greeting: str):
st.session_state.messages = [{"role": "assistant", "content": greeting}]
def export_chat_as_text(messages) -> str:
lines = []
for m in messages:
speaker = "You" if m["role"] == "user" else "Assistant"
lines.append(f"{speaker}: {m['content']}")
return "\n\n".join(lines)
# -----------------------------
# App UI
# -----------------------------
st.set_page_config(page_title="Simple Streamlit Chatbot", page_icon="💬", layout="centered")
st.title("💬 Simple Streamlit Chatbot")
with st.sidebar:
st.header("Settings")
mode = st.radio(
"Bot mode",
["Friendly", "Echo", "Calculator", "Small talk"],
index=0,
help="Choose how the bot should respond.",
key="mode_selection",
)
# Initialize state and allow clearing chat
if "messages" not in st.session_state:
init_chat("Hi! I'm a simple chatbot. Ask me something, or try me in Calculator mode.")
if st.button("Clear conversation", type="primary", use_container_width=True):
init_chat("Chat cleared. How can I help you now?")
st.rerun()
# Download conversation
chat_text = export_chat_as_text(st.session_state.messages)
st.download_button(
"Download conversation",
data=chat_text.encode("utf-8"),
file_name="conversation.txt",
mime="text/plain",
use_container_width=True,
)
st.caption("This chatbot runs locally with simple rule-based logic.")
# -----------------------------
# Message history
# -----------------------------
for msg in st.session_state.messages:
with st.chat_message(msg["role"]):
st.write(msg["content"])
# -----------------------------
# Input box
# -----------------------------
prompt = st.chat_input("Type your message here...")
if prompt:
# Store the user's message
st.session_state.messages.append({"role": "user", "content": prompt})
# Generate and store the assistant's reply
reply = generate_reply(prompt, st.session_state.get("mode_selection", "Friendly"))
st.session_state.messages.append({"role": "assistant", "content": reply})
# Rerun to display the new messages
st.rerun()