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()