Update progetto
This commit is contained in:
@@ -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"}
|
||||
|
||||
|
||||
@@ -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"}
|
||||
|
||||
@@ -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
@@ -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")
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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))
|
||||
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
import logging
|
||||
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s"
|
||||
)
|
||||
|
||||
logger = logging.getLogger("app")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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))
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
@@ -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))
|
||||
|
||||
Reference in New Issue
Block a user