Fix streamlit model, fix model selection display

This commit is contained in:
Samuele E. Locatelli
2025-09-05 09:11:28 +00:00
parent 8a37c2c474
commit c259266922
14 changed files with 679 additions and 99 deletions
+17 -17
View File
@@ -13,34 +13,41 @@ from config import settings
router = APIRouter()
@router.get("/models", response_model=List[str])
async def list_models():
try:
async with httpx.AsyncClient(timeout=settings.REQUEST_TIMEOUT) as client:
resp = await client.get(settings.LM_STUDIO_MODELS)
resp.raise_for_status()
data = resp.json()
return [m["id"] for m in data.get("data", [])]
except Exception:
logger.exception("Error fetching models")
raise HTTPException(status_code=500, detail="Failed to fetch models")
@router.post("/chat", response_model=ChatResponse)
async def chat_endpoint(payload: ChatRequest):
try:
# Creazione sessione se non esiste
session_id = payload.session_id
if not session_id:
meta = redis_service.create_session(payload.user_id, payload.message)
session_id = meta["session_id"]
# Salva messaggio utente
redis_service.save_chat(payload.user_id, session_id, {"role": "user", "content": payload.message})
# Prepara la history ottimizzata
history_to_send = await prepare_history(payload.user_id, session_id)
# Chiamata a LM Studio
model_to_use = payload.model_name or settings.MODEL_NAME
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_to_send},
json={"model": model_to_use, "messages": history_to_send},
)
resp.raise_for_status()
data = resp.json()
reply = data["choices"][0]["message"]["content"]
# Salva risposta assistant
redis_service.save_chat(payload.user_id, session_id, {"role": "assistant", "content": reply})
return ChatResponse(response=reply, session_id=session_id)
@@ -52,20 +59,14 @@ async def chat_endpoint(payload: ChatRequest):
@router.post("/chat-stream")
async def chat_stream_endpoint(payload: ChatRequest):
"""
Streams model output token-by-token usando SSE,
con windowing + summarization + condensed history.
"""
session_id = payload.session_id
if not session_id:
meta = redis_service.create_session(payload.user_id, payload.message)
session_id = meta["session_id"]
# Salva messaggio utente
redis_service.save_chat(payload.user_id, session_id, {"role": "user", "content": payload.message})
# Prepara la history ottimizzata
history_to_send = await prepare_history(payload.user_id, session_id)
model_to_use = payload.model_name or settings.MODEL_NAME
async def event_generator():
assistant_text = ""
@@ -75,7 +76,7 @@ async def chat_stream_endpoint(payload: ChatRequest):
"POST",
settings.LM_STUDIO_URL,
json={
"model": settings.MODEL_NAME,
"model": model_to_use,
"messages": history_to_send,
"stream": True
}
@@ -139,4 +140,3 @@ async def delete_history(
redis_service.clear_chat(user_id, session_id)
return {"status": "cleared"}
+12 -9
View File
@@ -1,6 +1,6 @@
# api/v1/sessions.py
from typing import List
from typing import List, Optional
from fastapi import Body, Query, Path, WebSocket, WebSocketDisconnect
from services import redis_service
from fastapi import APIRouter
@@ -17,11 +17,9 @@ router = APIRouter()
async def sessions_ws(websocket: WebSocket, user_id: str = Query(...)):
await websocket.accept()
try:
# Invia subito la lista completa
sessions = redis_service.get_sessions(user_id)
await websocket.send_json({"type": "full_list", "sessions": sessions})
# Sottoscrizione al canale Redis
pubsub = redis_service.r.pubsub()
channel = f"sessions:{user_id}"
await pubsub.subscribe(channel)
@@ -59,10 +57,10 @@ async def get_session_meta_endpoint(
@router.post("/sessions", response_model=dict)
async def create_session_endpoint(
user_id: str = Query(..., description="User ID"),
first_message: str = Body("", embed=True)
first_message: str = Body("", embed=True),
model_name: Optional[str] = Body(None, embed=True) # <-- Accept model_name
):
meta = redis_service.create_session(user_id, first_message)
# Notifica WS
meta = redis_service.create_session(user_id, first_message, model_name=model_name)
redis_service.r.publish(
f"sessions:{user_id}",
json.dumps({"type": "created", "session": meta})
@@ -73,9 +71,15 @@ async def create_session_endpoint(
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)
session_name: Optional[str] = Body(None, embed=True),
model_name: Optional[str] = Body(None, embed=True) # <-- Allow model update
):
updated = redis_service.update_session_meta(user_id, session_id, session_name=session_name) or {}
updated = redis_service.update_session_meta(
user_id, session_id,
session_name=session_name,
model_name=model_name
) or {}
if updated:
redis_service.r.publish(
f"sessions:{user_id}",
@@ -95,4 +99,3 @@ async def delete_session_endpoint(
)
return {"status": "deleted"}
+5 -2
View File
@@ -1,3 +1,4 @@
# config.py
import os
from pydantic_settings import BaseSettings
@@ -6,9 +7,11 @@ class Settings(BaseSettings):
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")
LM_STUDIO_MODELS: str = os.getenv("LM_STUDIO_URL", "http://10.74.83.100:1234/v1/models")
MODEL_NAME: str = os.getenv("MODEL_NAME", "qwen/qwen3-4b-2507")
REQUEST_TIMEOUT: float = float(os.getenv("REQUEST_TIMEOUT", 30.0))
#MODEL_NAME: str = os.getenv("MODEL_NAME", "qwen/qwen3-4b-thinking-2507")
#MODEL_NAME: str = os.getenv("MODEL_NAME", "openai/gpt-oss-20b")
REQUEST_TIMEOUT: float = float(os.getenv("REQUEST_TIMEOUT", 60.0))
settings = Settings()
+1
View File
@@ -7,6 +7,7 @@ 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
model_name: Optional[str] = None # <-- Add this
class ChatResponse(BaseModel):
response: str # assistant's reply
+1
View File
@@ -9,4 +9,5 @@ class SessionMeta(BaseModel):
session_name: str
message_count: int = 0
history_size_bytes: int = 0
model_name: Optional[str] = None # <-- Added field
+2 -1
View File
@@ -76,7 +76,8 @@ def create_session(user_id: str, first_message: str) -> dict:
"created_at": created_at,
"session_name": session_name,
"message_count": 0,
"history_size_bytes": 0
"history_size_bytes": 0,
"model_name": model_name # <-- Add this line
}
meta_key = f"chatSession:{user_id}:{session_id}"
index_key = f"chatSessionsIndex:{user_id}"
+3 -2
View File
@@ -10,7 +10,7 @@ import "katex/dist/katex.min.css"; // <-- IMPORTANTE
export default function App() {
const { messages, loading, sendMessage, stopGenerating, setMessages } = useChatStream();
const [themeName, setThemeName] = useState("light");
const [sessionName, setSessionName] = useState("");
const [sessionName, setSessionName, sessionModelName ] = useState("");
const theme = themes[themeName];
const userId = getUserId();
const sessionId = getSessionId();
@@ -103,7 +103,8 @@ export default function App() {
onSelectSession={handleSelectSession}
userId={userId}
sessionId={sessionId}
sessionName={sessionName} // <-- aggiunto
sessionModelName={sessionModelName}
sessionName={sessionName}
/>
);
}
+193
View File
@@ -0,0 +1,193 @@
// src/AssistantMessage.jsx
import React, { useState, useEffect } from "react";
import ReactMarkdown from "react-markdown";
import remarkGfm from "remark-gfm";
import remarkMath from "remark-math";
import rehypeKatex from "rehype-katex";
// Prism.js per syntax highlight
import Prism from "prismjs";
import "prismjs/themes/prism.css";
// Linguaggi base
import "prismjs/components/prism-sql";
import "prismjs/components/prism-javascript";
import "prismjs/components/prism-css";
import "prismjs/components/prism-json";
import "prismjs/components/prism-markdown";
import "prismjs/components/prism-csharp";
import "prismjs/components/prism-lua";
import "prismjs/components/prism-c";
import "prismjs/components/prism-cpp";
import "prismjs/components/prism-python";
import "prismjs/components/prism-basic";
import "prismjs/components/prism-javascript";
// Aggiungo alias "vb" che punta a "vbnet"
Prism.languages.vb = Prism.languages.vbnet;
export default function AssistantMessage({
content,
theme,
timestamp,
startedAt,
endedAt,
isFinal
}) {
const [showThink, setShowThink] = useState(false);
const [fadeOut, setFadeOut] = useState(false);
const ts = timestamp ?? endedAt ?? startedAt;
const thinkMatch = content?.match(/<think>([\s\S]*?)<\/think>/i);
const thinkContent = thinkMatch ? thinkMatch[1].trim() : null;
const isComplete = isFinal || Boolean(timestamp || endedAt);
const visibleContent = isComplete
? content?.replace(/<think>[\s\S]*?<\/think>/i, "").trim()
: content;
useEffect(() => {
if (thinkContent && !isComplete) {
setShowThink(true);
setFadeOut(false);
}
if (thinkContent && isComplete) {
setFadeOut(true);
const timer = setTimeout(() => setShowThink(false), 600);
return () => clearTimeout(timer);
}
}, [thinkContent, isComplete]);
return (
<div className="mb-2 text-start">
<div
className={`d-inline-block p-2 rounded ${theme.assistantBg}`}
style={{ maxWidth: "75%" }}
>
{showThink && (
<div
className={`think-block${fadeOut ? " fade-out" : ""}`}
style={{
fontStyle: "italic",
opacity: 0.7,
marginBottom: "0.5rem",
whiteSpace: "pre-wrap"
}}
>
🤔 {thinkContent}
</div>
)}
<ReactMarkdown
remarkPlugins={[remarkGfm, remarkMath]}
rehypePlugins={[rehypeKatex]}
components={{
table: (props) => (
<table className="table table-sm table-bordered" {...props} />
),
th: (props) => <th className="bg-light" {...props} />,
code: CodeWithCopy
}}
>
{visibleContent}
</ReactMarkdown>
</div>
{ts != null && (
<div
style={{
fontSize: "0.75rem",
color: "#666",
marginTop: "0.2rem",
marginLeft: "0.25rem"
}}
>
{formatDateTime(ts)}
</div>
)}
</div>
);
}
function CodeWithCopy({ inline, className = "", children, ...props }) {
const [copied, setCopied] = useState(false);
const codeText = String(children).replace(/\n$/, "");
const isFencedBlock = !inline && /^language-/.test(className);
// Evidenziazione con Prism
useEffect(() => {
if (isFencedBlock) {
Prism.highlightAll();
}
}, [codeText, isFencedBlock]);
if (!isFencedBlock) {
return (
<code className={className} {...props}>
{children}
</code>
);
}
const handleCopy = async () => {
try {
await navigator.clipboard.writeText(codeText);
setCopied(true);
setTimeout(() => setCopied(false), 1500);
} catch (err) {
console.error("Copy failed", err);
}
};
return (
<div style={{ position: "relative" }}>
<pre className={className} {...props} style={{ paddingRight: "2rem" }}>
<code className={className}>{codeText}</code>
</pre>
<button
onClick={handleCopy}
style={{
position: "absolute",
top: "0.25rem",
right: "0.25rem",
border: "none",
background: "transparent",
cursor: "pointer"
}}
className="btn btn-copy shadow"
title="Copy to clipboard"
>
📋
</button>
{copied && (
<span
style={{
position: "absolute",
top: "0.25rem",
right: "2rem",
fontSize: "0.8rem",
color: "green"
}}
>
Copied!
</span>
)}
</div>
);
}
function formatDateTime(dateTime) {
const date = dateTime instanceof Date ? dateTime : new Date(dateTime);
if (Number.isNaN(date.getTime())) return String(dateTime);
return date.toLocaleString(undefined, {
year: "numeric",
month: "short",
day: "numeric",
hour: "2-digit",
minute: "2-digit",
second: "2-digit"
});
}
+70 -42
View File
@@ -1,13 +1,16 @@
// src/ChatInput.jsx
import React, { useState, useEffect, useRef } from "react";
import axios from "axios";
const MAX_EXECUTION_TIME_MS_DEFAULT = 2 * 60 * 1000; // 2 minuti
const MAX_EXECUTION_TIME_MS_DEFAULT = 2 * 60 * 1000;
const EXTRA_TIME_MS = 60 * 1000;
const CHAR_LIMIT_FOR_TEXTAREA = 80;
const LINE_LIMIT_FOR_TEXTAREA = 1;
export default function ChatInput({ onSend, onStop, loading }) {
export default function ChatInput({ onSend, onStop, loading, sessionModelName = "" }) {
const [inputValue, setInputValue] = useState("");
const [modelList, setModelList] = useState([]);
const [selectedModel, setSelectedModel] = useState(sessionModelName || "");
const [timeLeft, setTimeLeft] = useState(null);
const [elapsedTime, setElapsedTime] = useState(0);
const [maxExecutionTime, setMaxExecutionTime] = useState(MAX_EXECUTION_TIME_MS_DEFAULT);
@@ -21,7 +24,21 @@ export default function ChatInput({ onSend, onStop, loading }) {
inputValue.length > CHAR_LIMIT_FOR_TEXTAREA ||
inputValue.split("\n").length > LINE_LIMIT_FOR_TEXTAREA;
// Focus automatico
// Fetch models and set default from session
useEffect(() => {
axios.get("/v1/models")
.then(res => {
const sortedModels = res.data.sort((a, b) => a.localeCompare(b));
setModelList(sortedModels);
if (sessionModelName && sortedModels.includes(sessionModelName)) {
setSelectedModel(sessionModelName);
}
})
.catch(err => {
console.error("❌ Failed to fetch models:", err.message);
});
}, [sessionModelName]);
useEffect(() => {
if (!loading && inputRef.current) {
inputRef.current.focus();
@@ -30,7 +47,6 @@ export default function ChatInput({ onSend, onStop, loading }) {
}
}, [isTextarea, loading]);
// Autoresize
useEffect(() => {
if (isTextarea && inputRef.current) {
inputRef.current.style.height = "auto";
@@ -38,7 +54,6 @@ export default function ChatInput({ onSend, onStop, loading }) {
}
}, [inputValue, isTextarea]);
// Gestione timer e countdown
useEffect(() => {
if (loading) {
setTimeLeft(Math.floor(maxExecutionTime / 1000));
@@ -77,7 +92,7 @@ export default function ChatInput({ onSend, onStop, loading }) {
const handleSend = () => {
if (inputValue.trim()) {
onSend(inputValue);
onSend(inputValue, selectedModel);
setInputValue("");
}
};
@@ -113,36 +128,55 @@ export default function ChatInput({ onSend, onStop, loading }) {
};
return (
<div className="chat-input-container p-2 border-top d-flex align-items-center">
{loading ? (
<div className="flex-grow-1 me-2 p-2 bg-light rounded small text-muted">
💬 Il modello sta processando (tempo trascorso: {elapsedTime}s)
</div>
) : isTextarea ? (
<textarea
ref={inputRef}
className="form-control me-2"
placeholder="Scrivi un messaggio..."
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
onKeyDown={handleKeyDown}
rows={3}
style={{ resize: "none", overflow: "hidden" }}
/>
) : (
<input
ref={inputRef}
type="text"
className="form-control me-2"
placeholder="Scrivi un messaggio..."
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
onKeyDown={handleKeyDown}
/>
)}
<div className="chat-input-container p-2 border-top">
<div className="input-group">
<select
className="form-select"
value={selectedModel}
onChange={(e) => setSelectedModel(e.target.value)}
style={{ maxWidth: "20%" }}
>
<option value="">Seleziona modello</option>
{modelList.map((model) => (
<option key={model} value={model}>
{model}
</option>
))}
</select>
{loading ? (
<div className="d-flex align-items-center">
{isTextarea ? (
<textarea
ref={inputRef}
className="form-control"
placeholder="Scrivi un messaggio..."
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
onKeyDown={handleKeyDown}
rows={3}
style={{ resize: "none", overflow: "hidden" }}
/>
) : (
<input
ref={inputRef}
type="text"
className="form-control"
placeholder="Scrivi un messaggio..."
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
onKeyDown={handleKeyDown}
/>
)}
<button className="btn btn-primary" onClick={handleSend}>
Invia
</button>
</div>
{loading && (
<div className="mt-2 d-flex align-items-center">
<div className="me-2 small text-muted">
💬 Il modello sta processando ({elapsedTime}s)
</div>
<button className="btn btn-danger btn-sm me-2" onClick={onStop}>
Stop
</button>
@@ -150,17 +184,11 @@ export default function ChatInput({ onSend, onStop, loading }) {
+1 min
</button>
{timeLeft !== null && (
<span
className={`small fw-bold ${timeLeft <= 10 ? "text-danger" : "text-muted"}`}
>
<span className={`small fw-bold ${timeLeft <= 10 ? "text-danger" : "text-muted"}`}>
{timeLeft}s
</span>
)}
</div>
) : (
<button className="btn btn-primary btn-sm" onClick={handleSend}>
Invia
</button>
)}
</div>
);
+3 -1
View File
@@ -20,6 +20,7 @@ export default function ChatLayout({
onSelectSession,
userId,
sessionId,
sessionModelName,
sessionName
}) {
const [showSessionsPanel, setShowSessionsPanel] = useState(false);
@@ -70,9 +71,10 @@ export default function ChatLayout({
{/* INPUT SEMPRE IN BASSO */}
{sessionId ? (
<ChatInput
onSend={onSend}
onSend={(message, modelName) => onSend(message, modelName)}
onStop={onStop}
loading={loading}
sessionModelName={sessionModelName || ""}
/>
) : (
<NoSessionBox onCreateSession={onCreateSession} />
+280
View File
@@ -5,6 +5,7 @@
"packages": {
"": {
"dependencies": {
"axios": "^1.11.0",
"prismjs": "^1.30.0",
"react-syntax-highlighter": "^15.6.6"
}
@@ -33,6 +34,36 @@
"integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==",
"license": "MIT"
},
"node_modules/asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
"license": "MIT"
},
"node_modules/axios": {
"version": "1.11.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.11.0.tgz",
"integrity": "sha512-1Lx3WLFQWm3ooKDYZD1eXmoGO9fxYQjrycfHFC8P0sCfQVXyROp0p9PFWBehewBOdCwHc+f/b8I0fMto5eSfwA==",
"license": "MIT",
"dependencies": {
"follow-redirects": "^1.15.6",
"form-data": "^4.0.4",
"proxy-from-env": "^1.1.0"
}
},
"node_modules/call-bind-apply-helpers": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
"function-bind": "^1.1.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/character-entities": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/character-entities/-/character-entities-1.2.4.tgz",
@@ -63,6 +94,18 @@
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"license": "MIT",
"dependencies": {
"delayed-stream": "~1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/comma-separated-tokens": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-1.0.8.tgz",
@@ -73,6 +116,74 @@
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
"license": "MIT",
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/dunder-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.1",
"es-errors": "^1.3.0",
"gopd": "^1.2.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-define-property": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-errors": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-object-atoms": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-set-tostringtag": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
"get-intrinsic": "^1.2.6",
"has-tostringtag": "^1.0.2",
"hasown": "^2.0.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/fault": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/fault/-/fault-1.0.4.tgz",
@@ -86,6 +197,42 @@
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/follow-redirects": {
"version": "1.15.11",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
"integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==",
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/RubenVerborgh"
}
],
"license": "MIT",
"engines": {
"node": ">=4.0"
},
"peerDependenciesMeta": {
"debug": {
"optional": true
}
}
},
"node_modules/form-data": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz",
"integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==",
"license": "MIT",
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"es-set-tostringtag": "^2.1.0",
"hasown": "^2.0.2",
"mime-types": "^2.1.12"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/format": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/format/-/format-0.2.2.tgz",
@@ -94,6 +241,103 @@
"node": ">=0.4.x"
}
},
"node_modules/function-bind": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/get-intrinsic": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.2",
"es-define-property": "^1.0.1",
"es-errors": "^1.3.0",
"es-object-atoms": "^1.1.1",
"function-bind": "^1.1.2",
"get-proto": "^1.0.1",
"gopd": "^1.2.0",
"has-symbols": "^1.1.0",
"hasown": "^2.0.2",
"math-intrinsics": "^1.1.0"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/get-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
"license": "MIT",
"dependencies": {
"dunder-proto": "^1.0.1",
"es-object-atoms": "^1.0.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/gopd": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/has-symbols": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/has-tostringtag": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
"license": "MIT",
"dependencies": {
"has-symbols": "^1.0.3"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/hasown": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
"license": "MIT",
"dependencies": {
"function-bind": "^1.1.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/hast-util-parse-selector": {
"version": "2.2.5",
"resolved": "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-2.2.5.tgz",
@@ -194,6 +438,36 @@
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/math-intrinsics": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/mime-types": {
"version": "2.1.35",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"license": "MIT",
"dependencies": {
"mime-db": "1.52.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/parse-entities": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-2.0.0.tgz",
@@ -233,6 +507,12 @@
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
"license": "MIT"
},
"node_modules/react": {
"version": "19.1.1",
"resolved": "https://registry.npmjs.org/react/-/react-19.1.1.tgz",
+1
View File
@@ -1,5 +1,6 @@
{
"dependencies": {
"axios": "^1.11.0",
"prismjs": "^1.30.0",
"react-syntax-highlighter": "^15.6.6"
}
+12 -25
View File
@@ -2,7 +2,7 @@
#### Streamlit Streaming using LM Studio as OpenAI Standin
#### run with `streamlit run app.py`
# !pip install pypdf langchain langchain-core langchain-openai
# !pip install pypdf langchain langchain_openai
import streamlit as st
from langchain_core.messages import AIMessage, HumanMessage
@@ -12,9 +12,10 @@ from langchain_core.prompts import ChatPromptTemplate
# app config
st.set_page_config(page_title="Egalware Chatbot", page_icon="🤖")
st.title("Egalware's Live Chatbot")
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:
@@ -22,18 +23,14 @@ def get_response(user_query, 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"
)
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 a generator for streaming
return chain.stream({
"chat_history": chat_history,
"user_question": user_query,
@@ -42,12 +39,11 @@ def get_response(user_query, chat_history):
# 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...)"),
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 history display
# conversation
for message in st.session_state.chat_history:
if isinstance(message, AIMessage):
with st.chat_message("AI"):
@@ -58,22 +54,13 @@ for message in st.session_state.chat_history:
# user input
user_query = st.chat_input("Type your message here...")
if user_query:
# store human message
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)
# stream AI response and capture it
with st.chat_message("AI"):
chunks = []
for chunk in get_response(user_query, st.session_state.chat_history):
st.write(chunk)
chunks.append(chunk)
full_response = "".join(chunks)
# store AI message with actual text
st.session_state.chat_history.append(AIMessage(content=full_response))
response = st.write_stream(get_response(user_query, st.session_state.chat_history))
st.session_state.chat_history.append(AIMessage(content=response))
+79
View File
@@ -0,0 +1,79 @@
####
#### Streamlit Streaming using LM Studio as OpenAI Standin
#### run with `streamlit run app.py`
# !pip install pypdf langchain langchain-core 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 Live 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 a generator for streaming
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 history display
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:
# store human message
st.session_state.chat_history.append(HumanMessage(content=user_query))
with st.chat_message("Human"):
st.markdown(user_query)
# stream AI response and capture it
with st.chat_message("AI"):
chunks = []
for chunk in get_response(user_query, st.session_state.chat_history):
st.write(chunk)
chunks.append(chunk)
full_response = "".join(chunks)
# store AI message with actual text
st.session_state.chat_history.append(AIMessage(content=full_response))