wynai commited on
Commit
5eb5327
·
verified ·
1 Parent(s): b1d9058

Upload 6 files

Browse files
Files changed (6) hide show
  1. README.md +45 -19
  2. app.py +355 -0
  3. auth.py +20 -0
  4. db.py +149 -0
  5. providers.py +66 -0
  6. requirements.txt +5 -3
README.md CHANGED
@@ -1,19 +1,45 @@
1
- ---
2
- title: ChatAI
3
- emoji: 🚀
4
- colorFrom: red
5
- colorTo: red
6
- sdk: docker
7
- app_port: 8501
8
- tags:
9
- - streamlit
10
- pinned: false
11
- short_description: Streamlit template space
12
- ---
13
-
14
- # Welcome to Streamlit!
15
-
16
- Edit `/src/streamlit_app.py` to customize this app to your heart's desire. :heart:
17
-
18
- If you have any questions, checkout our [documentation](https://docs.streamlit.io) and [community
19
- forums](https://discuss.streamlit.io).
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ChatAI Streamlit App (with Admin, OpenAI & Ollama)
2
+
3
+ A Streamlit app that provides:
4
+ - Rounded chat interface with **+** button to upload **files** or **images**
5
+ - Supports **OpenAI** and **Ollama** providers
6
+ - **Admin** role can manage user accounts
7
+ - Conversation history (SQLite)
8
+ - Export chat to Markdown
9
+
10
+ ## Quickstart
11
+
12
+ 1) **Create a virtual environment (recommended)**
13
+ ```bash
14
+ python -m venv .venv
15
+ source .venv/bin/activate # On Windows: .venv\Scripts\activate
16
+ ```
17
+
18
+ 2) **Install dependencies**
19
+ ```bash
20
+ pip install -r requirements.txt
21
+ ```
22
+
23
+ 3) **Run the app**
24
+ ```bash
25
+ streamlit run app.py
26
+ ```
27
+
28
+ 4) **Login**
29
+ On first run, a default admin account is created:
30
+ - **username**: `admin`
31
+ - **password**: `admin123`
32
+ 👉 Immediately change this in **Admin > Users**.
33
+
34
+ ## Providers
35
+
36
+ - **OpenAI**: Set your key in the sidebar **(OpenAI API Key)** or via env var `OPENAI_API_KEY`.
37
+ - **Ollama**: Make sure Ollama is running locally (default endpoint `http://localhost:11434`).
38
+ You can change endpoint in the sidebar.
39
+
40
+ ## Notes
41
+
42
+ - This is a reference implementation for local use. For production:
43
+ - Use a proper auth service (e.g., OAuth), secure key storage (e.g., Vault/KMS)
44
+ - Harden the admin endpoints and network access
45
+ - Add request limits, logging, and encryption-at-rest
app.py ADDED
@@ -0,0 +1,355 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os, io, base64, json
2
+ from datetime import datetime
3
+ import streamlit as st
4
+ from typing import List, Dict, Any
5
+
6
+ from db import init_db, get_user_by_username, create_conversation, list_conversations, rename_conversation, delete_conversation, add_message, get_messages, list_users, set_user_role, set_user_active, update_user_password
7
+ from auth import hash_password, verify_password, ensure_admin
8
+ from providers import OpenAIProvider, OllamaProvider, ProviderError
9
+
10
+ st.set_page_config(page_title="ChatAI (Streamlit)", page_icon="💬", layout="wide")
11
+
12
+ # --- CSS for rounded chat bubbles & '+' floating button ---
13
+ custom_css = """
14
+ <style>
15
+ .stChatMessage .stMarkdown {
16
+ border-radius: 16px;
17
+ padding: 12px 14px;
18
+ background: rgba(240, 242, 246, 0.6);
19
+ }
20
+ .stChatMessage[data-testid="stChatMessageUser"] .stMarkdown {
21
+ background: rgba(147, 197, 253, 0.25);
22
+ }
23
+ .plus-fab {
24
+ position: fixed;
25
+ bottom: 24px;
26
+ right: 24px;
27
+ width: 52px;
28
+ height: 52px;
29
+ border-radius: 50%;
30
+ background: #4F46E5;
31
+ color: white;
32
+ border: none;
33
+ box-shadow: 0 8px 24px rgba(0,0,0,0.15);
34
+ font-size: 28px;
35
+ cursor: pointer;
36
+ z-index: 9999;
37
+ }
38
+ .upload-card {
39
+ position: fixed;
40
+ bottom: 88px;
41
+ right: 24px;
42
+ width: 320px;
43
+ background: white;
44
+ border-radius: 16px;
45
+ box-shadow: 0 10px 30px rgba(0,0,0,0.15);
46
+ padding: 16px;
47
+ z-index: 9999;
48
+ border: 1px solid rgba(0,0,0,0.06);
49
+ }
50
+ </style>
51
+ """
52
+ st.markdown(custom_css, unsafe_allow_html=True)
53
+
54
+ # --- Initialize DB and default admin ---
55
+ init_db()
56
+ created_admin, admin_pwd = ensure_admin()
57
+
58
+ # --- Session State ---
59
+ if "user" not in st.session_state:
60
+ st.session_state.user = None
61
+ if "conversation_id" not in st.session_state:
62
+ st.session_state.conversation_id = None
63
+ if "show_uploader" not in st.session_state:
64
+ st.session_state.show_uploader = False
65
+ if "messages_cache" not in st.session_state:
66
+ st.session_state.messages_cache = []
67
+
68
+ def do_login(username, password):
69
+ user = get_user_by_username(username)
70
+ if not user or not user["is_active"]:
71
+ st.error("Sai tài khoản hoặc tài khoản đang bị khóa.")
72
+ return False
73
+ if verify_password(password, user["password_hash"]):
74
+ st.session_state.user = {"id": user["id"], "username": user["username"], "role": user["role"]}
75
+ return True
76
+ st.error("Mật khẩu không đúng.")
77
+ return False
78
+
79
+ def logout():
80
+ st.session_state.user = None
81
+ st.session_state.conversation_id = None
82
+ st.session_state.messages_cache = []
83
+
84
+ # --- Sidebar: Auth & Settings ---
85
+ with st.sidebar:
86
+ st.header("⚙️ Cấu hình")
87
+
88
+ if st.session_state.user:
89
+ st.success(f"Đã đăng nhập: **{st.session_state.user['username']}** ({st.session_state.user['role']})")
90
+ if st.button("Đăng xuất"):
91
+ logout()
92
+ st.rerun()
93
+ else:
94
+ st.subheader("Đăng nhập")
95
+ with st.form("login_form", clear_on_submit=False):
96
+ u = st.text_input("Tên đăng nhập")
97
+ p = st.text_input("Mật khẩu", type="password")
98
+ submitted = st.form_submit_button("Đăng nhập")
99
+ if submitted:
100
+ if do_login(u, p):
101
+ st.rerun()
102
+
103
+ st.divider()
104
+ st.subheader("Nhà cung cấp AI")
105
+ provider = st.selectbox("Provider", ["OpenAI", "Ollama"])
106
+ if provider == "OpenAI":
107
+ openai_key = st.text_input("OpenAI API Key", type="password", value=os.environ.get("OPENAI_API_KEY", ""))
108
+ model = st.text_input("Model", value="gpt-4o-mini")
109
+ else:
110
+ ollama_url = st.text_input("Ollama Endpoint", value="http://localhost:11434")
111
+ model = st.text_input("Model", value="llama3.1:8b")
112
+ temperature = st.slider("Temperature", 0.0, 1.0, 0.3, 0.05)
113
+
114
+ st.divider()
115
+ st.subheader("Tùy chọn")
116
+ sys_prompt = st.text_area("System Prompt (tùy chọn)", value="You are a helpful assistant. Answer in Vietnamese if the user speaks Vietnamese.")
117
+ if created_admin:
118
+ st.info(f"Admin mặc định đã được tạo. Tài khoản: admin / Mật khẩu: {admin_pwd} — hãy đổi ngay!")
119
+
120
+ # --- Page Router ---
121
+ def page_chat():
122
+ st.title("💬 ChatAI")
123
+
124
+ if not st.session_state.user:
125
+ st.info("Hãy đăng nhập để bắt đầu trò chuyện.")
126
+ return
127
+
128
+ # Conversations list
129
+ left, right = st.columns([1, 3])
130
+ with left:
131
+ st.subheader("🗂 Cuộc trò chuyện")
132
+ if st.button("➕ Tạo cuộc trò chuyện mới"):
133
+ st.session_state.conversation_id = create_conversation(st.session_state.user["id"], title="New Chat")
134
+ st.session_state.messages_cache = []
135
+ st.rerun()
136
+ convs = list_conversations(st.session_state.user["id"])
137
+ for c in convs:
138
+ selected = st.button(f"🗨 {c['title']}", key=f"conv_{c['id']}")
139
+ if selected:
140
+ st.session_state.conversation_id = c["id"]
141
+ st.session_state.messages_cache = get_messages(c["id"])
142
+ st.rerun()
143
+
144
+ with right:
145
+ if not st.session_state.conversation_id:
146
+ st.info("Chưa có cuộc trò chuyện. Hãy tạo mới bên trái.")
147
+ return
148
+
149
+ # Rename / Delete
150
+ cc1, cc2 = st.columns([3,1])
151
+ with cc1:
152
+ new_title = st.text_input("Tên cuộc trò chuyện", value="")
153
+ if st.button("Đổi tên"):
154
+ if new_title.strip():
155
+ rename_conversation(st.session_state.conversation_id, new_title.strip())
156
+ st.success("Đã đổi tên.")
157
+ else:
158
+ st.warning("Tên không hợp lệ.")
159
+ with cc2:
160
+ if st.button("🗑 Xóa cuộc trò chuyện", type="secondary"):
161
+ delete_conversation(st.session_state.conversation_id)
162
+ st.session_state.conversation_id = None
163
+ st.session_state.messages_cache = []
164
+ st.rerun()
165
+
166
+ # Chat history UI
167
+ msgs = st.session_state.messages_cache or get_messages(st.session_state.conversation_id)
168
+ for m in msgs:
169
+ role = m["role"]
170
+ content = m["content"]
171
+ with st.chat_message("assistant" if role=="assistant" else "user"):
172
+ st.markdown(content)
173
+ try:
174
+ atts = json.loads(m.get("attachments") or "[]")
175
+ for a in atts:
176
+ st.caption(f"📎 {a.get('name','file')} ({a.get('type','file')})")
177
+ except Exception:
178
+ pass
179
+
180
+ # Chat input
181
+ user_msg = st.chat_input("Nhập tin nhắn...")
182
+
183
+ # Floating '+' button for uploads
184
+ st.markdown('<button class="plus-fab" onclick="window.parent.postMessage({type:\\'toggle_uploader\\'}, \\\"*\\\")">+</button>', unsafe_allow_html=True)
185
+
186
+ # Fallback toggle
187
+ if st.button("Hiện/Tắt upload (fallback)"):
188
+ st.session_state.show_uploader = not st.session_state.show_uploader
189
+
190
+ if st.session_state.show_uploader:
191
+ with st.container():
192
+ st.markdown('<div class="upload-card">', unsafe_allow_html=True)
193
+ st.write("**Tải lên để đính kèm**")
194
+ file_uploader = st.file_uploader("Tệp (txt, pdf)", type=["txt","pdf"], accept_multiple_files=True, key="file_up")
195
+ img_uploader = st.file_uploader("Ảnh", type=["png","jpg","jpeg","webp"], accept_multiple_files=True, key="img_up")
196
+ if st.button("Đóng"):
197
+ st.session_state.show_uploader = False
198
+ st.markdown('</div>', unsafe_allow_html=True)
199
+
200
+ # Process send
201
+ if user_msg or (st.session_state.get("file_up") or st.session_state.get("img_up")):
202
+ attachments = []
203
+
204
+ def extract_text_from_file(f):
205
+ name = f.name
206
+ if name.lower().endswith(".txt"):
207
+ return f.read().decode("utf-8", errors="ignore")
208
+ if name.lower().endswith(".pdf"):
209
+ try:
210
+ import PyPDF2
211
+ reader = PyPDF2.PdfReader(io.BytesIO(f.read()))
212
+ pages = []
213
+ for p in reader.pages:
214
+ pages.append(p.extract_text() or "")
215
+ return "\\n".join(pages)
216
+ except Exception as e:
217
+ return f"[Không thể trích xuất PDF: {e}]"
218
+ return ""
219
+
220
+ uploaded_files = st.session_state.get("file_up") or []
221
+ uploaded_imgs = st.session_state.get("img_up") or []
222
+
223
+ context_snippets = []
224
+ for f in uploaded_files:
225
+ text = extract_text_from_file(f)
226
+ attachments.append({"name": f.name, "type": "file", "size": f.size})
227
+ if text:
228
+ context_snippets.append(f"### {f.name}\\n{text[:6000]}")
229
+
230
+ image_refs = []
231
+ for img in uploaded_imgs:
232
+ b64 = base64.b64encode(img.read()).decode("utf-8")
233
+ mime = "image/png" if img.name.lower().endswith("png") else "image/jpeg"
234
+ image_refs.append({"name": img.name, "type": "image", "b64": b64, "mime": mime})
235
+ attachments.append({"name": img.name, "type": "image", "size": img.size})
236
+
237
+ db_msgs = get_messages(st.session_state.conversation_id)
238
+ chat_history = [{"role": m["role"], "content": m["content"]} for m in db_msgs]
239
+
240
+ system_preamble = sys_prompt.strip() if sys_prompt else ""
241
+ if context_snippets:
242
+ system_preamble += "\\n\\n# File context (tóm tắt)\\n" + "\\n\\n".join(context_snippets)
243
+
244
+ messages_for_provider: List[Dict[str, Any]] = []
245
+ if system_preamble:
246
+ messages_for_provider.append({"role": "system", "content": system_preamble})
247
+
248
+ messages_for_provider.extend(chat_history[-12:])
249
+ if user_msg:
250
+ messages_for_provider.append({"role": "user", "content": user_msg})
251
+
252
+ try:
253
+ if provider == "OpenAI":
254
+ p = OpenAIProvider(api_key=openai_key if openai_key else None)
255
+ resp = p.generate(messages=messages_for_provider, model=model, temperature=temperature)
256
+ else:
257
+ p = OllamaProvider(base_url=ollama_url)
258
+ resp = p.generate(messages=messages_for_provider, model=model, temperature=temperature)
259
+ except ProviderError as e:
260
+ resp = f"Lỗi nhà cung cấp: {e}"
261
+ except Exception as e:
262
+ resp = f"Lỗi không xác định: {e}"
263
+
264
+ if user_msg:
265
+ add_message(st.session_state.conversation_id, "user", user_msg, attachments=json.dumps(attachments))
266
+ st.session_state.messages_cache.append({"role":"user","content":user_msg,"attachments":json.dumps(attachments)})
267
+ add_message(st.session_state.conversation_id, "assistant", resp)
268
+ st.session_state.messages_cache.append({"role":"assistant","content":resp,"attachments":"[]"})
269
+ st.session_state.show_uploader = False
270
+ st.rerun()
271
+
272
+ # Export
273
+ st.divider()
274
+ if st.button("⬇️ Xuất cuộc trò chuyện (Markdown)"):
275
+ msgs = get_messages(st.session_state.conversation_id)
276
+ md = ["# Lịch sử trò chuyện"]
277
+ for m in msgs:
278
+ who = "👤 User" if m["role"]=="user" else "🤖 Assistant"
279
+ md.append(f"**{who}**\\n\\n{m['content']}\\n")
280
+ b = "\\n\\n---\\n\\n".join(md).encode("utf-8")
281
+ st.download_button("Tải về .md", data=b, file_name="chat_history.md", mime="text/markdown")
282
+
283
+ def page_admin():
284
+ if not st.session_state.user or st.session_state.user["role"] != "admin":
285
+ st.warning("Chỉ Admin mới truy cập được trang này.")
286
+ return
287
+
288
+ st.title("🛠 Admin Panel")
289
+ st.subheader("Quản lý người dùng")
290
+
291
+ users = list_users()
292
+ for u in users:
293
+ cols = st.columns([2,1,1,1,2])
294
+ cols[0].write(f"**{u['username']}**")
295
+ cols[1].write(u["role"])
296
+ cols[2].write("✅" if u["is_active"] else "🚫")
297
+ if cols[3].button("Đổi vai trò", key=f"role_{u['id']}"):
298
+ new_role = "admin" if u["role"]=="user" else "user"
299
+ set_user_role(u["id"], new_role)
300
+ st.rerun()
301
+ with cols[4]:
302
+ c1, c2, c3 = st.columns(3)
303
+ if c1.button("Khóa/Mở", key=f"toggle_{u['id']}"):
304
+ set_user_active(u["id"], not u["is_active"])
305
+ st.rerun()
306
+ if c2.button("Đặt lại MK", key=f"reset_{u['id']}"):
307
+ newpw = st.text_input(f"Mật khẩu mới cho {u['username']}", key=f"pw_{u['id']}")
308
+ if newpw:
309
+ update_user_password(u["id"], hash_password(newpw))
310
+ st.success("Đã cập nhật mật khẩu.")
311
+ st.rerun()
312
+ if c3.button("Xóa", key=f"del_{u['id']}"):
313
+ from db import delete_user as delu
314
+ if u["username"]=="admin":
315
+ st.error("Không được xóa tài khoản admin gốc.")
316
+ else:
317
+ delu(u["id"])
318
+ st.rerun()
319
+
320
+ st.divider()
321
+ st.subheader("Thêm người dùng mới")
322
+ with st.form("new_user_form"):
323
+ nu = st.text_input("Tên đăng nhập")
324
+ np = st.text_input("Mật khẩu", type="password")
325
+ role = st.selectbox("Vai trò", ["user", "admin"])
326
+ submitted = st.form_submit_button("Tạo")
327
+ if submitted:
328
+ if not nu or not np:
329
+ st.error("Thiếu thông tin.")
330
+ elif get_user_by_username(nu):
331
+ st.error("Tên đăng nhập đã tồn tại.")
332
+ else:
333
+ from db import create_user
334
+ create_user(nu, hash_password(np), role=role, is_active=True)
335
+ st.success("Đã tạo người dùng.")
336
+ st.rerun()
337
+
338
+ # --- Main ---
339
+ tab = st.tabs(["💬 Chat", "🛠 Admin"])
340
+ with tab[0]:
341
+ page_chat()
342
+ with tab[1]:
343
+ page_admin()
344
+
345
+ # Small JS to listen to postMessage and toggle uploader (best-effort, uses the fallback button)
346
+ st.markdown(\"""
347
+ <script>
348
+ window.addEventListener('message', (event) => {
349
+ if (event.data && event.data.type === 'toggle_uploader') {
350
+ const btns = window.parent.document.querySelectorAll('button');
351
+ if (btns && btns.length > 0) { btns[btns.length-1].click(); }
352
+ }
353
+ }, false);
354
+ </script>
355
+ \""", unsafe_allow_html=True)
auth.py ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import bcrypt
2
+ from db import init_db, create_user, get_user_by_username
3
+
4
+ def hash_password(plain: str) -> bytes:
5
+ return bcrypt.hashpw(plain.encode("utf-8"), bcrypt.gensalt())
6
+
7
+ def verify_password(plain: str, hashed: bytes) -> bool:
8
+ try:
9
+ return bcrypt.checkpw(plain.encode("utf-8"), hashed)
10
+ except Exception:
11
+ return False
12
+
13
+ def ensure_admin():
14
+ # Ensure DB exists & default admin user
15
+ init_db()
16
+ if not get_user_by_username("admin"):
17
+ pwd = "admin123"
18
+ create_user("admin", hash_password(pwd), role="admin", is_active=True)
19
+ return True, pwd
20
+ return False, None
db.py ADDED
@@ -0,0 +1,149 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import sqlite3
2
+ from datetime import datetime
3
+
4
+ DB_PATH = "chatai.db"
5
+
6
+ def get_conn():
7
+ return sqlite3.connect(DB_PATH, check_same_thread=False)
8
+
9
+ def init_db():
10
+ conn = get_conn()
11
+ c = conn.cursor()
12
+ c.execute("""
13
+ CREATE TABLE IF NOT EXISTS users (
14
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
15
+ username TEXT UNIQUE NOT NULL,
16
+ password_hash BLOB NOT NULL,
17
+ role TEXT NOT NULL DEFAULT 'user',
18
+ is_active INTEGER NOT NULL DEFAULT 1,
19
+ created_at TEXT NOT NULL
20
+ )
21
+ """)
22
+ c.execute("""
23
+ CREATE TABLE IF NOT EXISTS conversations (
24
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
25
+ user_id INTEGER NOT NULL,
26
+ title TEXT,
27
+ created_at TEXT NOT NULL,
28
+ FOREIGN KEY(user_id) REFERENCES users(id)
29
+ )
30
+ """)
31
+ c.execute("""
32
+ CREATE TABLE IF NOT EXISTS messages (
33
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
34
+ conversation_id INTEGER NOT NULL,
35
+ role TEXT NOT NULL,
36
+ content TEXT NOT NULL,
37
+ attachments TEXT,
38
+ created_at TEXT NOT NULL,
39
+ FOREIGN KEY(conversation_id) REFERENCES conversations(id)
40
+ )
41
+ """)
42
+ conn.commit()
43
+ return conn
44
+
45
+ # --- User CRUD ---
46
+ def create_user(username, password_hash, role="user", is_active=True):
47
+ conn = get_conn()
48
+ c = conn.cursor()
49
+ c.execute(
50
+ "INSERT INTO users (username, password_hash, role, is_active, created_at) VALUES (?,?,?,?,?)",
51
+ (username, password_hash, role, 1 if is_active else 0, datetime.utcnow().isoformat()),
52
+ )
53
+ conn.commit()
54
+ return c.lastrowid
55
+
56
+ def get_user_by_username(username):
57
+ conn = get_conn()
58
+ c = conn.cursor()
59
+ c.execute("SELECT id, username, password_hash, role, is_active, created_at FROM users WHERE username = ?", (username,))
60
+ row = c.fetchone()
61
+ if not row: return None
62
+ return {"id": row[0], "username": row[1], "password_hash": row[2], "role": row[3], "is_active": bool(row[4]), "created_at": row[5]}
63
+
64
+ def get_user_by_id(user_id):
65
+ conn = get_conn()
66
+ c = conn.cursor()
67
+ c.execute("SELECT id, username, password_hash, role, is_active, created_at FROM users WHERE id = ?", (user_id,))
68
+ row = c.fetchone()
69
+ if not row: return None
70
+ return {"id": row[0], "username": row[1], "password_hash": row[2], "role": row[3], "is_active": bool(row[4]), "created_at": row[5]}
71
+
72
+ def list_users():
73
+ conn = get_conn()
74
+ c = conn.cursor()
75
+ c.execute("SELECT id, username, role, is_active, created_at FROM users ORDER BY id ASC")
76
+ rows = c.fetchall()
77
+ return [{"id": r[0], "username": r[1], "role": r[2], "is_active": bool(r[3]), "created_at": r[4]} for r in rows]
78
+
79
+ def set_user_active(user_id, active: bool):
80
+ conn = get_conn()
81
+ c = conn.cursor()
82
+ c.execute("UPDATE users SET is_active=? WHERE id=?", (1 if active else 0, user_id))
83
+ conn.commit()
84
+
85
+ def set_user_role(user_id, role: str):
86
+ conn = get_conn()
87
+ c = conn.cursor()
88
+ c.execute("UPDATE users SET role=? WHERE id=?", (role, user_id))
89
+ conn.commit()
90
+
91
+ def update_user_password(user_id, new_password_hash):
92
+ conn = get_conn()
93
+ c = conn.cursor()
94
+ c.execute("UPDATE users SET password_hash=? WHERE id=?", (new_password_hash, user_id))
95
+ conn.commit()
96
+
97
+ def delete_user(user_id):
98
+ conn = get_conn()
99
+ c = conn.cursor()
100
+ c.execute("DELETE FROM users WHERE id=?", (user_id,))
101
+ conn.commit()
102
+
103
+ # --- Conversations & Messages ---
104
+ def create_conversation(user_id, title="New Chat"):
105
+ conn = get_conn()
106
+ c = conn.cursor()
107
+ c.execute("INSERT INTO conversations (user_id, title, created_at) VALUES (?,?,?)",
108
+ (user_id, title, datetime.utcnow().isoformat()))
109
+ conn.commit()
110
+ return c.lastrowid
111
+
112
+ def list_conversations(user_id):
113
+ conn = get_conn()
114
+ c = conn.cursor()
115
+ c.execute("SELECT id, title, created_at FROM conversations WHERE user_id=? ORDER BY id DESC", (user_id,))
116
+ rows = c.fetchall()
117
+ return [{"id": r[0], "title": r[1], "created_at": r[2]} for r in rows]
118
+
119
+ def rename_conversation(conversation_id, title):
120
+ conn = get_conn()
121
+ c = conn.cursor()
122
+ c.execute("UPDATE conversations SET title=? WHERE id=?", (title, conversation_id))
123
+ conn.commit()
124
+
125
+ def delete_conversation(conversation_id):
126
+ conn = get_conn()
127
+ c = conn.cursor()
128
+ c.execute("DELETE FROM messages WHERE conversation_id=?", (conversation_id,))
129
+ c.execute("DELETE FROM conversations WHERE id=?", (conversation_id,))
130
+ conn.commit()
131
+
132
+ def add_message(conversation_id, role, content, attachments=None):
133
+ conn = get_conn()
134
+ c = conn.cursor()
135
+ c.execute("INSERT INTO messages (conversation_id, role, content, attachments, created_at) VALUES (?,?,?,?,?)",
136
+ (conversation_id, role, content, attachments or "[]", datetime.utcnow().isoformat()))
137
+ conn.commit()
138
+ return c.lastrowid
139
+
140
+ def get_messages(conversation_id):
141
+ conn = get_conn()
142
+ c = conn.cursor()
143
+ c.execute("SELECT role, content, attachments, created_at FROM messages WHERE conversation_id=? ORDER BY id ASC",
144
+ (conversation_id,))
145
+ rows = c.fetchall()
146
+ msgs = []
147
+ for r in rows:
148
+ msgs.append({"role": r[0], "content": r[1], "attachments": r[2], "created_at": r[3]})
149
+ return msgs
providers.py ADDED
@@ -0,0 +1,66 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os, requests
2
+ from typing import List, Dict, Any, Optional
3
+
4
+ # --- Provider base ---
5
+ class ProviderError(Exception):
6
+ pass
7
+
8
+ class ChatProvider:
9
+ def generate(self, messages: List[Dict[str, Any]], **kwargs) -> str:
10
+ raise NotImplementedError
11
+
12
+ # --- OpenAI ---
13
+ class OpenAIProvider(ChatProvider):
14
+ def __init__(self, api_key: Optional[str] = None):
15
+ self.api_key = api_key or os.environ.get("OPENAI_API_KEY")
16
+ if not self.api_key:
17
+ raise ProviderError("OpenAI API key missing. Provide it in the sidebar or set OPENAI_API_KEY.")
18
+
19
+ try:
20
+ # Use official library (>=1.0)
21
+ from openai import OpenAI
22
+ self.client = OpenAI(api_key=self.api_key)
23
+ except Exception as e:
24
+ raise ProviderError(f"OpenAI library error: {e}")
25
+
26
+ def generate(self, messages: List[Dict[str, Any]], model: str = "gpt-4o-mini", temperature: float = 0.3, **kwargs) -> str:
27
+ try:
28
+ resp = self.client.chat.completions.create(
29
+ model=model,
30
+ messages=messages,
31
+ temperature=temperature
32
+ )
33
+ return resp.choices[0].message.content or ""
34
+ except Exception as e:
35
+ raise ProviderError(f"OpenAI API error: {e}")
36
+
37
+ # --- Ollama ---
38
+ class OllamaProvider(ChatProvider):
39
+ def __init__(self, base_url: str = "http://localhost:11434"):
40
+ self.base_url = base_url.rstrip("/")
41
+
42
+ def generate(self, messages: List[Dict[str, Any]], model: str = "llama3.1:8b", temperature: float = 0.3, **kwargs) -> str:
43
+ url = f"{self.base_url}/api/chat"
44
+ payload = {"model": model, "messages": messages, "stream": False, "options": {"temperature": temperature}}
45
+ try:
46
+ r = requests.post(url, json=payload, timeout=120)
47
+ r.raise_for_status()
48
+ data = r.json()
49
+ msg = data.get("message", {}).get("content", "")
50
+ if not msg and "choices" in data:
51
+ msg = data["choices"][0]["message"]["content"]
52
+ return msg
53
+ except Exception as e:
54
+ raise ProviderError(f"Ollama API error: {e}")
55
+
56
+ # --- Helper ---
57
+ def convert_streamlit_messages_to_openai(messages: List[Dict[str, Any]]):
58
+ converted = []
59
+ for m in messages:
60
+ role = m.get("role", "user")
61
+ content = m.get("content", "")
62
+ attachments = m.get("attachments", [])
63
+ if attachments:
64
+ content += "\\n\\n[Attachments]\\n" + "\\n".join([f"- {a.get('name','file')} ({a.get('type','file')})" for a in attachments])
65
+ converted.append({"role": role, "content": content})
66
+ return converted
requirements.txt CHANGED
@@ -1,3 +1,5 @@
1
- altair
2
- pandas
3
- streamlit
 
 
 
1
+ streamlit>=1.32
2
+ openai>=1.37.0
3
+ requests>=2.31.0
4
+ bcrypt>=4.1.2
5
+ PyPDF2>=3.0.1