|
import os |
|
from fastapi import FastAPI, HTTPException, Request |
|
from sse_starlette.sse import EventSourceResponse |
|
from fastapi.staticfiles import StaticFiles |
|
from openai import AsyncOpenAI |
|
from fasthtml.common import FastHTML, Html, Head, Title, Body, Div, Button, Textarea, Script, Style, P, Favicon, ft_hx |
|
import bleach |
|
|
|
from styles import styles |
|
from script import script |
|
from fasthtml_hf import setup_hf_backup |
|
|
|
secret_key = os.getenv('SECRET_KEY') |
|
|
|
app = FastHTML(secret_key=secret_key) |
|
|
|
app.mount("/static", StaticFiles(directory="static"), name="static") |
|
|
|
setup_hf_backup(app) |
|
|
|
client = AsyncOpenAI() |
|
|
|
conversations = {} |
|
|
|
ALLOWED_TAGS = list(bleach.sanitizer.ALLOWED_TAGS) + ["h1", "h2", "h3", "p", "strong", "em", "ul", "ol", "li", "code", "pre", "blockquote"] |
|
ALLOWED_ATTRIBUTES = bleach.sanitizer.ALLOWED_ATTRIBUTES |
|
|
|
static_dir = os.path.join(os.path.dirname(__file__), "static") |
|
light_icon = os.path.join(static_dir, "favicon-light.ico") |
|
dark_icon = os.path.join(static_dir, "favicon-dark.ico") |
|
|
|
def Svg(*c, viewBox=None, **kwargs): |
|
return ft_hx('svg', *c, viewBox=viewBox, **kwargs) |
|
|
|
def Path(*c, d=None, fill=None, **kwargs): |
|
return ft_hx('path', *c, d=d, fill=fill, **kwargs) |
|
|
|
@app.get("/") |
|
def home(): |
|
"""Render homepage with FastGPT UI.""" |
|
home_text = """ |
|
## FastGPT - A ChatGPT Implementation Using FastHTML |
|
""" |
|
|
|
page = Html( |
|
Head( |
|
Title('FastGPT'), |
|
Favicon(light_icon="/static/favicon-light.ico", dark_icon="/static/favicon-dark.ico"), |
|
Style(styles), |
|
Script(src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"), |
|
Script(src="https://cdnjs.cloudflare.com/ajax/libs/dompurify/2.2.9/purify.min.js") |
|
), |
|
Body( |
|
Div( |
|
Div("FastGPT", _class="logo-text"), |
|
Div( |
|
Button( |
|
Svg( |
|
Path( |
|
d="M441 58.9L453.1 71c9.4 9.4 9.4 24.6 0 33.9L424 134.1 377.9 88 407 58.9c9.4-9.4 24.6-9.4 33.9 0zM209.8 256.2L344 121.9 390.1 168 255.8 302.2c-2.9 2.9-6.5 5-10.4 6.1l-58.5 16.7 16.7-58.5c1.1-3.9 3.2-7.5 6.1-10.4zM373.1 25L175.8 222.2c-8.7 8.7-15 19.4-18.3 31.1l-28.6 100c-2.4 8.4-.1 17.4 6.1 23.6s15.2 8.5 23.6 6.1l100-28.6c11.8-3.4 22.5-9.7 31.1-18.3L487 138.9c28.1-28.1 28.1-73.7 0-101.8L474.9 25C446.8-3.1 401.2-3.1 373.1 25zM88 64C39.4 64 0 103.4 0 152L0 424c0 48.6 39.4 88 88 88l272 0c48.6 0 88-39.4 88-88l0-112c0-13.3-10.7-24-24-24s-24 10.7-24 24l0 112c0 22.1-17.9 40-40 40L88 464c-22.1 0-40-17.9-40-40l0-272c0-22.1 17.9-40 40-40l112 0c13.3 0 24-10.7 24-24s-10.7-24-24-24L88 64z", |
|
fill="#b4b4b4" |
|
), |
|
viewBox="0 0 512 512", |
|
_class="refresh-icon" |
|
), |
|
onclick="location.reload()", |
|
_class="refresh-button" |
|
), |
|
_class='refresh-container' |
|
), |
|
_class="header" |
|
), |
|
Div( |
|
Div( |
|
Div(id="home-text-container", _class="markdown-container", **{"data-home-text": home_text}), |
|
_class='title-wrapper' |
|
), |
|
P(id='output'), |
|
Div( |
|
Textarea( |
|
id='message', |
|
rows=1, |
|
cols=50, |
|
placeholder="Message FastGPT", |
|
oninput="autoResizeTextarea()", |
|
onkeypress="checkEnter(event)" |
|
), |
|
Button( |
|
Svg( |
|
Path( |
|
d="M214.6 41.4c-12.5-12.5-32.8-12.5-45.3 0l-160 160c-12.5 12.5-12.5 32.8 0 45.3s32.8 12.5 45.3 0L160 141.2 160 448c0 17.7 14.3 32 32 32s32-14.3 32-32l0-306.7L329.4 246.6c12.5 12.5 32.8 12.5 45.3 0s12.5-32.8 0-45.3l-160-160z" |
|
), |
|
viewBox="0 0 384 512", |
|
_class="send-icon" |
|
), |
|
onclick="sendMessage()", |
|
_class="send-button" |
|
), |
|
_class="container" |
|
), |
|
_class="wrapper" |
|
), |
|
Script(script) |
|
) |
|
) |
|
return page |
|
|
|
@app.get("/stream") |
|
async def stream_response(request: Request, message: str, session_id: str = None): |
|
"""Stream responses for the given user input.""" |
|
if not message: |
|
raise HTTPException(status_code=400, detail="Message parameter is required") |
|
if not session_id: |
|
raise HTTPException(status_code=400, detail="Session ID is required") |
|
|
|
if session_id not in conversations: |
|
conversations[session_id] = [ |
|
{"role": "system", "content": "You are a helpful assistant. Use Markdown for formatting."} |
|
] |
|
|
|
conversations[session_id].append({"role": "user", "content": message}) |
|
|
|
async def event_generator(): |
|
try: |
|
response = await client.chat.completions.create( |
|
model="gpt-4o-mini", |
|
messages=conversations[session_id], |
|
stream=True |
|
) |
|
|
|
assistant_response = "" |
|
|
|
async for chunk in response: |
|
if await request.is_disconnected(): |
|
print(f"Client for session {session_id} disconnected") |
|
break |
|
|
|
content = chunk.choices[0].delta.content |
|
if content: |
|
assistant_response += content |
|
yield {"data": content} |
|
|
|
conversations[session_id].append({"role": "assistant", "content": assistant_response}) |
|
|
|
except Exception as e: |
|
yield {"data": f"Error: {str(e)}"} |
|
|
|
finally: |
|
print(f"Streaming finished for session {session_id}") |
|
|
|
return EventSourceResponse(event_generator()) |
|
|
|
@app.get("/reset") |
|
def reset_conversation(session_id: str): |
|
"""Reset the conversation for the specified session ID.""" |
|
if session_id in conversations: |
|
del conversations[session_id] |
|
return {"message": "Conversation reset."} |