File size: 4,233 Bytes
58a4232
 
 
 
fab8da5
 
58a4232
fab8da5
 
58a4232
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
fab8da5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
58a4232
 
 
fab8da5
 
 
 
 
 
58a4232
 
fab8da5
58a4232
fab8da5
 
 
 
 
 
 
 
 
 
 
 
 
 
58a4232
 
 
 
 
 
 
 
 
35ac688
58a4232
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
fab8da5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
58a4232
 
 
 
fab8da5
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from transformers import AutoTokenizer, AutoModelForCausalLM, pipeline
from pydantic import BaseModel
from collections import defaultdict
from threading import Lock
import uvicorn
import uuid
import time

app = FastAPI()

# Allow requests from any origin (so your HTML page can call this API)
app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],
    allow_methods=["*"],
    allow_headers=["*"],
)

# Load model + tokenizer
model_id = "Qwen/Qwen2.5-0.5B-Instruct"
tokenizer = AutoTokenizer.from_pretrained(model_id)
model = AutoModelForCausalLM.from_pretrained(model_id)

chat_pipeline = pipeline(
    "text-generation",
    model=model,
    tokenizer=tokenizer,
)

SYSTEM_PROMPT = (
    "You are Bella, a witty and friendly female AI assistant. "
    "Always respond as Bella, never use any other name. "
    "Keep your replies short, casual, and to the point."
)

# --- In-memory session history store ---------------------------------------
# NOTE: this resets whenever the process restarts. For real persistence across
# restarts/multiple server instances, swap this dict for Firestore/Supabase
# (you already use both elsewhere in Bella AI) — the shape below (a list of
# {"role", "content"} dicts per session_id) maps directly onto either.
MAX_TURNS = 12          # how many user+assistant turn-pairs to keep per session
SESSION_TTL_SECONDS = 60 * 60 * 6  # drop sessions idle for 6+ hours

_histories: dict[str, list[dict]] = defaultdict(list)
_last_seen: dict[str, float] = {}
_lock = Lock()


def _prune_stale_sessions():
    now = time.time()
    stale = [sid for sid, ts in _last_seen.items() if now - ts > SESSION_TTL_SECONDS]
    for sid in stale:
        _histories.pop(sid, None)
        _last_seen.pop(sid, None)


class ChatRequest(BaseModel):
    message: str
    session_id: str | None = None  # client can pass one to resume a conversation


class ChatResponse(BaseModel):
    response: str
    session_id: str


@app.post("/chat", response_model=ChatResponse)
async def chat(request: ChatRequest):
    with _lock:
        _prune_stale_sessions()

        session_id = request.session_id or str(uuid.uuid4())
        history = _histories[session_id]

        # Build the message list: system + priming turn + saved history + new message
        messages = [
            {"role": "system", "content": SYSTEM_PROMPT},
            {"role": "user", "content": "Who are you?"},
            {"role": "assistant", "content": "I'm Bella! What can I help you with?"},
            *history,
            {"role": "user", "content": request.message},
        ]

    prompt = tokenizer.apply_chat_template(
        messages,
        tokenize=False,
        add_generation_prompt=True,
    )

    outputs = chat_pipeline(
        prompt,
        max_new_tokens=256,
        do_sample=True,
        temperature=0.2,
        top_p=0.9,
        repetition_penalty=1.2,
        pad_token_id=tokenizer.eos_token_id,
    )

    generated_text = outputs[0]["generated_text"]

    # Strip off the prompt, leaving only the new reply
    response = generated_text[len(prompt):].strip()

    # Safety net: cut off if the model starts hallucinating a new turn
    for stop_token in ["<|im_start|>", "<|im_end|>", "User:", "system"]:
        if stop_token in response:
            response = response.split(stop_token)[0].strip()

    if not response:
        response = "Hmm, I didn't quite catch that — can you rephrase?"

    with _lock:
        history = _histories[session_id]
        history.append({"role": "user", "content": request.message})
        history.append({"role": "assistant", "content": response})
        # keep only the last MAX_TURNS turn-pairs
        if len(history) > MAX_TURNS * 2:
            _histories[session_id] = history[-MAX_TURNS * 2:]
        _last_seen[session_id] = time.time()

    return {"response": response, "session_id": session_id}


@app.post("/chat/reset")
async def reset(session_id: str):
    with _lock:
        _histories.pop(session_id, None)
        _last_seen.pop(session_id, None)
    return {"status": "ok"}


if __name__ == "__main__":
    uvicorn.run(app, host="0.0.0.0", port=7860)