Update progetto

This commit is contained in:
Samuele E. Locatelli
2025-09-03 06:42:58 +00:00
parent fb54ebf116
commit 3aea8d9db8
14 changed files with 798 additions and 260 deletions
+143
View File
@@ -0,0 +1,143 @@
# api/v1/chat.py
import httpx
import json
import asyncio
from fastapi import APIRouter, HTTPException, Query
from fastapi.responses import StreamingResponse
from typing import List, Dict, Any, Optional
from models.chat import ChatRequest, ChatResponse
from services import redis_service # now using updated service with session support
from utils.logging import logger
from config import settings
router = APIRouter()
MAX_HISTORY_LENGTH = 50
@router.post("/chat", response_model=ChatResponse)
async def chat_endpoint(payload: ChatRequest):
try:
# Create a new session if session_id not provided
session_id = payload.session_id
if not session_id:
meta = redis_service.create_session(payload.user_id, payload.message)
session_id = meta["session_id"]
# Save user message
redis_service.save_chat(payload.user_id, session_id, {"role": "user", "content": payload.message})
history = redis_service.get_chat(payload.user_id, session_id, limit=MAX_HISTORY_LENGTH)
async with httpx.AsyncClient(timeout=settings.REQUEST_TIMEOUT) as client:
resp = await client.post(
settings.LM_STUDIO_URL,
json={"model": settings.MODEL_NAME, "messages": history},
)
resp.raise_for_status()
data = resp.json()
reply = data["choices"][0]["message"]["content"]
# Save assistant message
redis_service.save_chat(payload.user_id, session_id, {"role": "assistant", "content": reply})
# Return normal ChatResponse, but could also include session_id if needed
return ChatResponse(response=reply, session_id=session_id)
except Exception:
logger.exception("Error in /chat endpoint")
raise HTTPException(status_code=500, detail="Internal server error")
@router.post("/chat-stream")
async def chat_stream_endpoint(payload: ChatRequest):
"""
Streams model output token-by-token using SSE.
"""
session_id = payload.session_id
if not session_id:
meta = redis_service.create_session(payload.user_id, payload.message)
session_id = meta["session_id"]
redis_service.save_chat(payload.user_id, session_id, {"role": "user", "content": payload.message})
history = redis_service.get_chat(payload.user_id, session_id, limit=MAX_HISTORY_LENGTH)
async def event_generator():
assistant_text = ""
try:
async with httpx.AsyncClient(timeout=None) as client:
async with client.stream(
"POST",
settings.LM_STUDIO_URL,
json={
"model": settings.MODEL_NAME,
"messages": history,
"stream": True
}
) as r:
async for raw_line in r.aiter_lines():
if not raw_line:
continue
line = raw_line if raw_line.startswith("data:") else f"data: {raw_line}"
payload_str = line[len("data: "):].strip()
if payload_str == "[DONE]":
yield "data: [DONE]\n\n"
break
yield f"data: {payload_str}\n\n"
try:
obj = json.loads(payload_str)
choice = obj.get("choices", [{}])[0]
delta = choice.get("delta", {})
piece = delta.get("content") or choice.get("text")
if piece:
assistant_text += piece
except json.JSONDecodeError:
pass
await asyncio.sleep(0)
except Exception as e:
logger.exception("Streaming error in /chat-stream")
yield f"event: error\ndata: {str(e)}\n\n"
finally:
if assistant_text:
redis_service.save_chat(payload.user_id, session_id, {"role": "assistant", "content": assistant_text})
headers = {
"Cache-Control": "no-cache",
"Connection": "keep-alive",
"X-Accel-Buffering": "no"
}
return StreamingResponse(event_generator(), media_type="text/event-stream", headers=headers)
@router.get("/history")
async def get_history(
user_id: str = Query(..., description="User ID"),
session_id: str = Query(..., description="Session ID"),
limit: int = Query(MAX_HISTORY_LENGTH, description="Max number of messages to return")
) -> List[Dict[str, Any]]:
"""
Return all history saved for a given user/session.
"""
logger.info(f"[GET /history] user_id={user_id}, session_id={session_id}, limit={limit}")
history = redis_service.get_chat(user_id, session_id, limit=limit)
return history or []
@router.delete("/history")
async def delete_history(
user_id: str = Query(..., description="User ID"),
session_id: str = Query(..., description="Session ID")
):
"""
Clears history for a given user/session.
"""
logger.info(f"[DELETE /history] user_id={user_id}, session_id={session_id}")
redis_service.clear_chat(user_id, session_id)
return {"status": "cleared"}
+63
View File
@@ -0,0 +1,63 @@
# api/v1/sessions.py
from typing import List
from fastapi import Body, Query, Path
from services import redis_service
from fastapi import APIRouter
from models.session import SessionMeta
router = APIRouter()
@router.post("/sessions", response_model=dict)
async def create_session_endpoint(
user_id: str = Query(..., description="User ID"),
first_message: str = Body("", embed=True)
):
"""
Create a new chat session for a user.
Session name is taken from `first_message` or defaults to "New Chat".
"""
meta = redis_service.create_session(user_id, first_message)
return meta
@router.get("/sessions", response_model=List[dict])
async def list_sessions_endpoint(
user_id: str = Query(..., description="User ID")
):
"""
Get a list of all saved sessions for a user (most recent first).
"""
return redis_service.get_sessions(user_id)
@router.get("/sessions/{session_id}", response_model=dict)
async def get_session_meta_endpoint(
user_id: str = Query(..., description="User ID"),
session_id: str = Path(..., description="Session ID")
):
"""
Get metadata for a single session.
"""
return redis_service.get_session_meta(user_id, session_id) or {}
@router.patch("/sessions/{session_id}", response_model=dict)
async def update_session_endpoint(
user_id: str = Query(..., description="User ID"),
session_id: str = Path(..., description="Session ID"),
session_name: str = Body(..., embed=True)
):
"""
Rename a session or update metadata fields.
"""
return redis_service.update_session_meta(user_id, session_id, session_name=session_name) or {}
@router.delete("/sessions/{session_id}", response_model=dict)
async def delete_session_endpoint(
user_id: str = Query(..., description="User ID"),
session_id: str = Path(..., description="Session ID")
):
"""
Delete a session and its chat history.
"""
redis_service.delete_session(user_id, session_id)
return {"status": "deleted"}
+14
View File
@@ -0,0 +1,14 @@
import os
from pydantic_settings import BaseSettings
class Settings(BaseSettings):
REDIS_HOST: str = os.getenv("REDIS_HOST", "localhost")
REDIS_PORT: int = int(os.getenv("REDIS_PORT", 6379))
REDIS_DB: int = int(os.getenv("REDIS_DB", 0))
LM_STUDIO_URL: str = os.getenv("LM_STUDIO_URL", "http://10.74.83.100:1234/v1/chat/completions")
MODEL_NAME: str = os.getenv("MODEL_NAME", "qwen/qwen3-4b-thinking-2507")
#MODEL_NAME: str = os.getenv("MODEL_NAME", "qwen/qwen3-4b-2507")
REQUEST_TIMEOUT: float = float(os.getenv("REQUEST_TIMEOUT", 30.0))
settings = Settings()
+6 -96
View File
@@ -1,19 +1,7 @@
# main.py
from fastapi import FastAPI, Request
# main.py (new project entrypoint)
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import StreamingResponse
import redis
import json
import httpx
r = redis.Redis(host='localhost', port=6379, db=1)
def save_chat(user_id, message):
r.rpush(f"chat:{user_id}", json.dumps(message))
def get_chat(user_id):
messages = r.lrange(f"chat:{user_id}", 0, -1)
return [json.loads(m.decode('utf-8')) for m in messages]
from api.v1 import chat, sessions # import the routers
app = FastAPI()
@@ -24,84 +12,6 @@ app.add_middleware(
allow_headers=["*"],
)
LM_STUDIO_URL = "http://10.74.83.100:1234/v1/chat/completions"
MODEL_NAME = "qwen/qwen3-4b-2507" # update as needed
@app.post("/chat")
async def chat(request: Request):
data = await request.json()
user_id = data.get("user_id", "default")
message = data["message"]
save_chat(user_id, {"role": "user", "content": message})
history = get_chat(user_id)
async with httpx.AsyncClient(timeout=None) as client:
resp = await client.post(LM_STUDIO_URL, json={
"model": MODEL_NAME,
"messages": history,
})
result = resp.json()
reply = result["choices"][0]["message"]["content"]
save_chat(user_id, {"role": "assistant", "content": reply})
return {"response": reply}
@app.post("/chat-stream")
async def chat_stream(request: Request):
data = await request.json()
user_id = data.get("user_id", "default")
message = data["message"]
# Save user message and build history
save_chat(user_id, {"role": "user", "content": message})
history = get_chat(user_id)
async def event_generator():
assistant_text = ""
try:
async with httpx.AsyncClient(timeout=None) as client:
async with client.stream("POST", LM_STUDIO_URL, json={
"model": MODEL_NAME,
"messages": history,
"stream": True
}) as r:
async for raw_line in r.aiter_lines():
if not raw_line:
continue
# Normalize to standard SSE "data: ..." form
line = raw_line if raw_line.startswith("data:") else f"data: {raw_line}"
payload = line[len("data: "):].strip()
if payload == "[DONE]":
# Finalize and flush
yield "data: [DONE]\n\n"
break
# Echo the SSE line to client
yield f"data: {payload}\n\n"
# Accumulate content for saving to Redis
try:
obj = json.loads(payload)
choice = obj.get("choices", [{}])[0]
# Handle OpenAI-style streaming objects
delta = choice.get("delta", {})
piece = delta.get("content")
if piece is None:
# Some servers send "text" instead
piece = choice.get("text")
if piece:
assistant_text += piece
except Exception:
# Ignore non-JSON control lines
pass
finally:
if assistant_text:
save_chat(user_id, {"role": "assistant", "content": assistant_text})
headers = {
"Cache-Control": "no-cache",
"Connection": "keep-alive",
"X-Accel-Buffering": "no" # helps if behind Nginx
}
return StreamingResponse(event_generator(), media_type="text/event-stream", headers=headers)
# Mount the router under /v1
app.include_router(chat.router, prefix="/v1")
app.include_router(sessions.router, prefix="/v1")
+13
View File
@@ -0,0 +1,13 @@
# models/chat.py
from pydantic import BaseModel
from datetime import datetime
from typing import List, Optional
class ChatRequest(BaseModel):
user_id: str # identifier for the user (can be same as session if desired)
session_id: Optional[str] = None # new: multi-session handling
message: str # user input text
class ChatResponse(BaseModel):
response: str # assistant's reply
session_id: str # <-- now included in every response
+12
View File
@@ -0,0 +1,12 @@
# models/session.py
from pydantic import BaseModel
from datetime import datetime
from typing import List, Optional
class SessionMeta(BaseModel):
session_id: str
created_at: datetime
session_name: str
message_count: int = 0
history_size_bytes: int = 0
+12
View File
@@ -0,0 +1,12 @@
import httpx
from ..config import settings
async def send_chat_completion(history: list):
async with httpx.AsyncClient(timeout=settings.REQUEST_TIMEOUT) as client:
resp = await client.post(
settings.LM_STUDIO_URL,
json={"model": settings.MODEL_NAME, "messages": history},
)
resp.raise_for_status()
return resp.json()
+152
View File
@@ -0,0 +1,152 @@
# services/redis_service.py
import redis
import json
import uuid
from config import settings
from datetime import datetime
from typing import List, Optional
r = redis.Redis(
host=settings.REDIS_HOST,
port=settings.REDIS_PORT,
db=settings.REDIS_DB,
decode_responses=True
)
# -------------------------
# Chat message operations
# -------------------------
def save_chat(user_id: str, session_id: str, message: dict):
"""
Save a message for a user/session, adding a UTC ISO timestamp.
Also updates session metadata (message_count, history_size_bytes).
"""
key = f"chatHistory:{user_id}:{session_id}"
enriched = {
**message,
"timestamp": datetime.utcnow().isoformat() + "Z"
}
r.rpush(key, json.dumps(enriched))
# Update metadata after adding
_update_session_stats(user_id, session_id)
def get_chat(user_id: str, session_id: str, limit: Optional[int] = None):
"""
Retrieve messages for a user/session, sorted by timestamp.
Optionally limit to the last N messages.
"""
key = f"chatHistory:{user_id}:{session_id}"
data = r.lrange(key, 0, -1)
messages = [json.loads(item) for item in data]
messages.sort(key=lambda m: m.get("timestamp", ""))
if limit:
messages = messages[-limit:]
return messages
def clear_chat(user_id: str, session_id: str):
"""
Delete all messages for a user/session.
Also resets metadata stats (message_count, history_size_bytes).
"""
key = f"chatHistory:{user_id}:{session_id}"
r.delete(key)
_update_session_stats(user_id, session_id, reset=True)
# -------------------------
# Session metadata ops
# -------------------------
def create_session(user_id: str, first_message: str) -> dict:
"""
Create a new session with initial metadata.
"""
session_id = str(uuid.uuid4())
created_at = datetime.utcnow().isoformat() + "Z"
session_name = (first_message.strip()[:50] or "New Chat")
meta = {
"session_id": session_id,
"created_at": created_at,
"session_name": session_name,
"message_count": 0,
"history_size_bytes": 0
}
# Save metadata
r.set(f"chatSession:{user_id}:{session_id}", json.dumps(meta))
# Add to per-user index (sorted set with score = timestamp)
r.zadd(f"chatSessionsIndex:{user_id}", {session_id: datetime.utcnow().timestamp()})
return meta
def get_sessions(user_id: str) -> List[dict]:
"""
Return all sessions for a user, newest first.
"""
session_ids = r.zrevrangebyscore(f"chatSessionsIndex:{user_id}", "+inf", "-inf")
sessions = []
for sid in session_ids:
raw = r.get(f"chatSession:{user_id}:{sid}")
if raw:
sessions.append(json.loads(raw))
return sessions
def get_session_meta(user_id: str, session_id: str) -> Optional[dict]:
raw = r.get(f"chatSession:{user_id}:{session_id}")
return json.loads(raw) if raw else None
def update_session_meta(user_id: str, session_id: str, **updates) -> Optional[dict]:
"""
Update metadata fields (e.g., session_name) for a session.
"""
meta = get_session_meta(user_id, session_id)
if not meta:
return None
meta.update(updates)
r.set(f"chatSession:{user_id}:{session_id}", json.dumps(meta))
return meta
def delete_session(user_id: str, session_id: str):
"""
Delete metadata, chat history, and remove from index.
"""
r.delete(f"chatSession:{user_id}:{session_id}")
r.delete(f"chatHistory:{user_id}:{session_id}")
r.zrem(f"chatSessionsIndex:{user_id}", session_id)
# -------------------------
# Internal helpers
# -------------------------
def _update_session_stats(user_id: str, session_id: str, reset: bool = False):
"""
Refresh message_count and history_size_bytes in metadata.
"""
meta = get_session_meta(user_id, session_id)
if not meta:
return
if reset:
meta["message_count"] = 0
meta["history_size_bytes"] = 0
else:
# Count messages and total size
key = f"chatHistory:{user_id}:{session_id}"
messages = r.lrange(key, 0, -1)
meta["message_count"] = len(messages)
meta["history_size_bytes"] = sum(len(m.encode("utf-8")) for m in messages)
r.set(f"chatSession:{user_id}:{session_id}", json.dumps(meta))
+9
View File
@@ -0,0 +1,9 @@
import logging
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s"
)
logger = logging.getLogger("app")
+8
View File
@@ -0,0 +1,8 @@
## Streamlit chatbot client
Client Chatbot con update realtime che sfrutta le API di openAI per connettersi a LM Studio locale
esempio tratto inizialmente da qui:
https://github.com/ingridstevens/AI-projects/tree/main/streamlit-streaming-langchain
+66
View File
@@ -0,0 +1,66 @@
####
#### Streamlit Streaming using LM Studio as OpenAI Standin
#### run with `streamlit run app.py`
# !pip install pypdf langchain langchain_openai
import streamlit as st
from langchain_core.messages import AIMessage, HumanMessage
from langchain_openai import ChatOpenAI
from langchain_core.output_parsers import StrOutputParser
from langchain_core.prompts import ChatPromptTemplate
# app config
st.set_page_config(page_title="Egalware Chatbot", page_icon="🤖")
st.title("Egalware's Chatbot")
def get_response(user_query, chat_history):
template = """
You are a helpful assistant. Answer the following questions considering the history of the conversation:
Chat history: {chat_history}
User question: {user_question}
"""
prompt = ChatPromptTemplate.from_template(template)
# Using LM Studio Local Inference Server
llm = ChatOpenAI(base_url="http://10.74.83.100:1234/v1",api_key="lm-studio", model="qwen/qwen3-4b-2507")
chain = prompt | llm | StrOutputParser()
return chain.stream({
"chat_history": chat_history,
"user_question": user_query,
})
# session state
if "chat_history" not in st.session_state:
st.session_state.chat_history = [
AIMessage(content="Hello, I am EgalWare's current ChatBot. How can I help you? (puoi fare domande in italiano, ma in inglese funziona meglio...)"),
]
# conversation
for message in st.session_state.chat_history:
if isinstance(message, AIMessage):
with st.chat_message("AI"):
st.write(message.content)
elif isinstance(message, HumanMessage):
with st.chat_message("Human"):
st.write(message.content)
# user input
user_query = st.chat_input("Type your message here...")
if user_query is not None and user_query != "":
st.session_state.chat_history.append(HumanMessage(content=user_query))
with st.chat_message("Human"):
st.markdown(user_query)
with st.chat_message("AI"):
response = st.write_stream(get_response(user_query, st.session_state.chat_history))
st.session_state.chat_history.append(AIMessage(content=response))
+186
View File
@@ -0,0 +1,186 @@
import streamlit as st
import streamlit.components.v1 as components
import redis
import json
import uuid
import hashlib
from langchain_core.messages import AIMessage, HumanMessage
from langchain_openai import ChatOpenAI
from langchain_core.output_parsers import StrOutputParser
from langchain_core.prompts import ChatPromptTemplate
# ---------------------
# Redis connection
# ---------------------
r = redis.Redis(host="localhost", port=6379, decode_responses=True)
# ---------------------
# Session ID helpers
# ---------------------
def get_or_set_session_id():
"""Get or set session_id from browser localStorage and rerun on first set."""
if "session_id" in st.session_state:
return st.session_state.session_id
components.html(f"""
<script>
const key = 'egalware_session_id';
let sid = window.localStorage.getItem(key);
if (!sid) {{
sid = '{uuid.uuid4()}';
window.localStorage.setItem(key, sid);
}}
const streamlitDoc = window.parent.document;
let hidden = streamlitDoc.querySelector('#session_id_input_hidden');
if (!hidden) {{
hidden = document.createElement('input');
hidden.type = 'hidden';
hidden.id = 'session_id_input_hidden';
streamlitDoc.body.appendChild(hidden);
}}
hidden.value = sid;
hidden.dispatchEvent(new Event('input', {{ bubbles: true }}));
</script>
""", height=0)
sid = st.text_input("session_id_input_hidden", label_visibility="collapsed", key="session_id_input_hidden")
if sid and sid != st.session_state.get("session_id"):
st.session_state.session_id = sid
st.rerun()
return st.session_state.get("session_id")
# ---------------------
# Chat history persistence
# ---------------------
def load_history(session_id):
raw = r.get(f"chatbot:history:{session_id}")
if raw:
messages = json.loads(raw)
return [
AIMessage(content=m["content"]) if m["type"] == "ai"
else HumanMessage(content=m["content"])
for m in messages
]
return [AIMessage(content="Hello, I am EgalWare's ChatBot. How can I help you? (puoi fare domande in italiano, ma in inglese funziona meglio...)")]
def save_history(session_id, history):
messages = [
{"type": "ai" if isinstance(m, AIMessage) else "human", "content": m.content}
for m in history
]
r.set(f"chatbot:history:{session_id}", json.dumps(messages))
r.expire(f"chatbot:history:{session_id}", 60*60*24*7)
def delete_history(session_id):
r.delete(f"chatbot:history:{session_id}")
for key in r.scan_iter(f"chatbot:cache:{session_id}:*"):
r.delete(key)
# ---------------------
# Caching
# ---------------------
def get_cache_key(session_id, prompt):
digest = hashlib.sha256(prompt.encode()).hexdigest()
return f"chatbot:cache:{session_id}:{digest}"
def get_cached_response(session_id, prompt):
return r.get(get_cache_key(session_id, prompt))
def set_cached_response(session_id, prompt, response):
r.setex(get_cache_key(session_id, prompt), 300, response)
# ---------------------
# LLM
# ---------------------
def get_response(session_id, user_query, chat_history):
cached = get_cached_response(session_id, user_query)
if cached:
yield cached
return
prompt = ChatPromptTemplate.from_template(
"You are a helpful assistant. Answer the following considering the history:\n\n"
"Chat history: {chat_history}\n\nUser question: {user_question}"
)
llm = ChatOpenAI(
base_url="http://10.74.83.100:1234/v1",
api_key="lm-studio",
model="qwen/qwen3-4b-2507"
)
chain = prompt | llm | StrOutputParser()
full_resp = ""
for chunk in chain.stream({
"chat_history": chat_history,
"user_question": user_query
}):
full_resp += chunk
yield chunk
set_cached_response(session_id, user_query, full_resp)
# ---------------------
# UI Layout
# ---------------------
st.set_page_config(page_title="Egalware Chatbot", page_icon="🤖")
session_id = get_or_set_session_id()
# Optional: user label separate from session_id
user_label = st.text_input("Optional display name (does not affect session ID):", key="user_label")
# Sticky header CSS
st.markdown("""
<style>
.sticky-header {
position: sticky;
top: 0;
background-color: white;
padding-top: 0.5rem;
padding-bottom: 0.5rem;
z-index: 999;
border-bottom: 1px solid #ddd;
}
</style>
""", unsafe_allow_html=True)
# Header with clear button
st.markdown('<div class="sticky-header">', unsafe_allow_html=True)
col_title, col_btn = st.columns([0.9, 0.1])
with col_title:
st.title("Egalware's Chatbot")
with col_btn:
if st.button("🗑️", help="Clear conversation", type="secondary"):
delete_history(session_id)
st.session_state.chat_history = load_history(session_id)
st.rerun()
st.markdown('</div>', unsafe_allow_html=True)
# Initialize history
if not session_id:
if "chat_history" not in st.session_state:
st.session_state.chat_history = [AIMessage(content="Initializing session… please wait")]
else:
if "chat_history" not in st.session_state:
st.session_state.chat_history = load_history(session_id)
# Display messages
for message in st.session_state.chat_history:
role = "AI" if isinstance(message, AIMessage) else "Human"
with st.chat_message(role):
st.write(message.content)
# Input for chat
user_query = st.chat_input("Type your message here…")
if session_id and user_query:
st.session_state.chat_history.append(HumanMessage(content=user_query))
with st.chat_message("Human"):
st.markdown(user_query)
with st.chat_message("AI"):
response_text = st.write_stream(
get_response(session_id, user_query, st.session_state.chat_history)
)
st.session_state.chat_history.append(AIMessage(content=response_text))
save_history(session_id, st.session_state.chat_history)
+66
View File
@@ -0,0 +1,66 @@
####
#### Streamlit Streaming using LM Studio as OpenAI Standin
#### run with `streamlit run app.py`
# !pip install pypdf langchain langchain_openai
import streamlit as st
from langchain_core.messages import AIMessage, HumanMessage
from langchain_openai import ChatOpenAI
from langchain_core.output_parsers import StrOutputParser
from langchain_core.prompts import ChatPromptTemplate
# app config
st.set_page_config(page_title="Egalware Chatbot", page_icon="🤖")
st.title("Egalware's Chatbot")
def get_response(user_query, chat_history):
template = """
You are a helpful assistant. Answer the following questions considering the history of the conversation:
Chat history: {chat_history}
User question: {user_question}
"""
prompt = ChatPromptTemplate.from_template(template)
# Using LM Studio Local Inference Server
llm = ChatOpenAI(base_url="http://10.74.83.100:1234/v1",api_key="lm-studio", model="qwen/qwen3-4b-2507")
chain = prompt | llm | StrOutputParser()
return chain.stream({
"chat_history": chat_history,
"user_question": user_query,
})
# session state
if "chat_history" not in st.session_state:
st.session_state.chat_history = [
AIMessage(content="Hello, I am EgalWare's current ChatBot. How can I help you? (puoi fare domande in italiano, ma in inglese funziona meglio...)"),
]
# conversation
for message in st.session_state.chat_history:
if isinstance(message, AIMessage):
with st.chat_message("AI"):
st.write(message.content)
elif isinstance(message, HumanMessage):
with st.chat_message("Human"):
st.write(message.content)
# user input
user_query = st.chat_input("Type your message here...")
if user_query is not None and user_query != "":
st.session_state.chat_history.append(HumanMessage(content=user_query))
with st.chat_message("Human"):
st.markdown(user_query)
with st.chat_message("AI"):
response = st.write_stream(get_response(user_query, st.session_state.chat_history))
st.session_state.chat_history.append(AIMessage(content=response))
+48 -164
View File
@@ -1,182 +1,66 @@
####
#### Streamlit Streaming using LM Studio as OpenAI Standin
#### run with `streamlit run app.py`
# !pip install pypdf langchain langchain_openai
import streamlit as st
import streamlit.components.v1 as components
import redis
import json
import uuid
import hashlib
from langchain_core.messages import AIMessage, HumanMessage
from langchain_openai import ChatOpenAI
from langchain_core.output_parsers import StrOutputParser
from langchain_core.prompts import ChatPromptTemplate
# ---------------------
# Redis connection
# ---------------------
r = redis.Redis(host="localhost", port=6379, decode_responses=True)
# ---------------------
# Session ID helpers
# ---------------------
def get_or_set_session_id():
"""Get or set session_id from browser localStorage and trigger rerun when first set."""
if "session_id" in st.session_state:
return st.session_state.session_id
components.html(f"""
<script>
const key = 'egalware_session_id';
let sid = window.localStorage.getItem(key);
if (!sid) {{
sid = '{uuid.uuid4()}';
window.localStorage.setItem(key, sid);
}}
const streamlitDoc = window.parent.document;
let hidden = streamlitDoc.querySelector('#session_id_input');
if (!hidden) {{
hidden = document.createElement('input');
hidden.type = 'hidden';
hidden.id = 'session_id_input';
streamlitDoc.body.appendChild(hidden);
}}
hidden.value = sid;
hidden.dispatchEvent(new Event('input', {{ bubbles: true }}));
</script>
""", height=0)
sid = st.text_input("session_id_input", label_visibility="collapsed")
if sid and sid != st.session_state.get("session_id"):
st.session_state.session_id = sid
st.rerun() # immediately rerun with the new ID
return st.session_state.get("session_id")
# ---------------------
# Chat history persistence
# ---------------------
def load_history(session_id):
raw = r.get(f"chatbot:history:{session_id}")
if raw:
messages = json.loads(raw)
return [
AIMessage(content=m["content"]) if m["type"] == "ai"
else HumanMessage(content=m["content"])
for m in messages
]
return [AIMessage(content="Hello, I am EgalWare's ChatBot. How can I help you? (puoi fare domande in italiano, ma in inglese funziona meglio...)")]
def save_history(session_id, history):
messages = [
{"type": "ai" if isinstance(m, AIMessage) else "human", "content": m.content}
for m in history
]
r.set(f"chatbot:history:{session_id}", json.dumps(messages))
r.expire(f"chatbot:history:{session_id}", 60*60*24*7)
def delete_history(session_id):
r.delete(f"chatbot:history:{session_id}")
for key in r.scan_iter(f"chatbot:cache:{session_id}:*"):
r.delete(key)
# ---------------------
# Caching
# ---------------------
def get_cache_key(session_id, prompt):
digest = hashlib.sha256(prompt.encode()).hexdigest()
return f"chatbot:cache:{session_id}:{digest}"
def get_cached_response(session_id, prompt):
return r.get(get_cache_key(session_id, prompt))
def set_cached_response(session_id, prompt, response):
r.setex(get_cache_key(session_id, prompt), 300, response)
# ---------------------
# LLM
# ---------------------
def get_response(session_id, user_query, chat_history):
cached = get_cached_response(session_id, user_query)
if cached:
yield cached
return
prompt = ChatPromptTemplate.from_template(
"You are a helpful assistant. Answer the following considering the history:\n\n"
"Chat history: {chat_history}\n\nUser question: {user_question}"
)
llm = ChatOpenAI(
base_url="http://10.74.83.100:1234/v1",
api_key="lm-studio",
model="qwen/qwen3-4b-2507"
)
chain = prompt | llm | StrOutputParser()
full_resp = ""
for chunk in chain.stream({
"chat_history": chat_history,
"user_question": user_query
}):
full_resp += chunk
yield chunk
set_cached_response(session_id, user_query, full_resp)
# ---------------------
# UI
# ---------------------
# app config
st.set_page_config(page_title="Egalware Chatbot", page_icon="🤖")
session_id = get_or_set_session_id()
st.title("Egalware's Live Chatbot")
# Sticky header CSS
st.markdown("""
<style>
.sticky-header {
position: sticky;
top: 0;
background-color: white;
padding-top: 0.5rem;
padding-bottom: 0.5rem;
z-index: 999;
border-bottom: 1px solid #ddd;
}
</style>
""", unsafe_allow_html=True)
def get_response(user_query, chat_history):
# Header
st.markdown('<div class="sticky-header">', unsafe_allow_html=True)
col_title, col_btn = st.columns([0.9, 0.1])
with col_title:
st.title("Egalware's Chatbot")
with col_btn:
if st.button("🗑️", help="Clear conversation", type="secondary"):
delete_history(session_id)
st.session_state.chat_history = load_history(session_id)
st.rerun()
st.markdown('</div>', unsafe_allow_html=True)
template = """
You are a helpful assistant. Answer the following questions considering the history of the conversation:
# If still no ID, display placeholder history so UI doesn't look empty
if not session_id:
if "chat_history" not in st.session_state:
st.session_state.chat_history = [AIMessage(content="Initializing session… please wait")]
else:
if "chat_history" not in st.session_state:
st.session_state.chat_history = load_history(session_id)
Chat history: {chat_history}
# Conversation display
User question: {user_question}
"""
prompt = ChatPromptTemplate.from_template(template)
# Using LM Studio Local Inference Server
llm = ChatOpenAI(base_url="http://10.74.83.100:1234/v1",api_key="lm-studio", model="qwen/qwen3-4b-2507")
chain = prompt | llm | StrOutputParser()
return chain.stream({
"chat_history": chat_history,
"user_question": user_query,
})
# session state
if "chat_history" not in st.session_state:
st.session_state.chat_history = [
AIMessage(content="Hello, I am EgalWare's Live & Stateless ChatBot. How can I help you? (puoi fare domande in italiano, ma in inglese funziona meglio...)"),
]
# conversation
for message in st.session_state.chat_history:
role = "AI" if isinstance(message, AIMessage) else "Human"
with st.chat_message(role):
st.write(message.content)
if isinstance(message, AIMessage):
with st.chat_message("AI"):
st.write(message.content)
elif isinstance(message, HumanMessage):
with st.chat_message("Human"):
st.write(message.content)
# Input
user_query = st.chat_input("Type your message here")
if session_id and user_query:
# user input
user_query = st.chat_input("Type your message here...")
if user_query is not None and user_query != "":
st.session_state.chat_history.append(HumanMessage(content=user_query))
with st.chat_message("Human"):
st.markdown(user_query)
with st.chat_message("AI"):
response_text = st.write_stream(
get_response(session_id, user_query, st.session_state.chat_history)
)
st.session_state.chat_history.append(AIMessage(content=response_text))
save_history(session_id, st.session_state.chat_history)
response = st.write_stream(get_response(user_query, st.session_state.chat_history))
st.session_state.chat_history.append(AIMessage(content=response))