File size: 6,882 Bytes
f250a9c
 
 
 
 
 
7aa8120
 
f250a9c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
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()