from flask import Flask, request, render_template_string, Response, stream_with_context
from llama_cpp import Llama
import os
import time
import json
import re
import sqlite3
from datetime import datetime
app = Flask(__name__)
# --- ১. সঠিক মডেল লোকেশন ---
# Q1_0 (এন্ড-টু-এন্ড ১-বিট) ফরম্যাট এখন mainline llama.cpp-তে upstream সাপোর্টেড,
# তাই llama-cpp-python (রিসেন্ট ভার্সন) দিয়ে এটা সরাসরি চলে।
# (শুধু তাদের ternary/Q2_0 ফরম্যাটের জন্য বিশেষ ফর্ক লাগে, Q1_0-এর জন্য না।)
# ✅ বর্তমানে সক্রিয়: Bonsai 8B (~1.16 GB)
MODEL_REPO = "prism-ml/Bonsai-8B-gguf"
MODEL_FILE = "Bonsai-8B-Q1_0.gguf"
# 🔽 ছোট মডেল চাইলে উপরের দুই লাইন কমেন্ট করে নিচের যেকোনো একটা আনকমেন্ট করুন 🔽
# --- Bonsai 4B (~0.57 GB) — মাঝারি সাইজ, আরও দ্রুত ---
#MODEL_REPO = "prism-ml/Bonsai-4B-gguf"
#MODEL_FILE = "Bonsai-4B-Q1_0.gguf"
# --- Bonsai 1.7B (~0.25 GB) — সবচেয়ে ছোট (১ বিলিয়নের কাছাকাছি), সবচেয়ে দ্রুত ---
#MODEL_REPO = "prism-ml/Bonsai-1.7B-gguf"
#MODEL_FILE = "Bonsai-1.7B-Q1_0.gguf"
print("⏳ মডেল ডাউনলোড হচ্ছে... (প্রথমবার একটু সময় লাগবে)")
from huggingface_hub import hf_hub_download
model_path = hf_hub_download(
repo_id=MODEL_REPO,
filename=MODEL_FILE,
local_dir="./models",
token=None # পাবলিক রিপো, টোকেন লাগবে না
)
print("✅ ডাউনলোড সম্পূর্ণ!")
print(f"📁 ফাইল লোকেশন: {model_path}")
print("⏳ মডেল লোড হচ্ছে (CPU)...")
try:
# llama-cpp-python: GGUF মেটাডেটা থেকে নিজেই আর্কিটেকচার (qwen3) বুঝে নেয়,
# তাই model_type জাতীয় কিছু দিতে হয় না।
# CPU-এর সব কোর ব্যবহার করা হচ্ছে যাতে prompt processing (prefill) দ্রুত হয়।
# n_threads -> টোকেন জেনারেশনের সময় ব্যবহৃত থ্রেড সংখ্যা
# n_threads_batch -> prompt/prefill প্রসেসিংয়ের সময় ব্যবহৃত থ্রেড সংখ্যা (এটাই প্রথম টোকেনের দেরির মূল কারণ)
cpu_count = os.cpu_count() or 4
llm = Llama(
model_path=model_path,
n_ctx=6144,
n_threads=cpu_count,
n_threads_batch=cpu_count,
n_batch=512, # prefill ব্যাচ সাইজ, বড় করলে prompt processing দ্রুত হয়
verbose=False
)
# --- ওয়ার্মআপ ---
# প্রথম ইনফারেন্স কলে llama.cpp কিছু অভ্যন্তরীণ বাফার/গ্রাফ তৈরি করে যা এক্সট্রা সময় নেয়।
# সার্ভার চালু হওয়ার সময়ই একটা ডামি জেনারেশন চালিয়ে সেই ওয়ান-টাইম খরচ আগেই সেরে ফেলা হচ্ছে,
# যাতে ইউজারের প্রথম আসল রিকোয়েস্টে এই পেনাল্টি না লাগে।
print("🔥 মডেল ওয়ার্মআপ হচ্ছে...")
list(llm("<|im_start|>user\nহাই<|im_end|>\n<|im_start|>assistant\n\n\n\n\n",
max_tokens=1, stream=True))
print("✅ ওয়ার্মআপ সম্পন্ন!")
print("✅ মডেল প্রস্তুত! সার্ভার চালু হচ্ছে...")
except Exception as e:
print(f"⚠️ llama-cpp-python লোড করতে সমস্যা: {e}")
llm = None
# --- ২. ডাটাবেজ (SQLite) ---
# ব্যবহারকারীর তথ্য (facts) সংরক্ষণের জন্য একটা সাধারণ SQLite ডাটাবেজ।
# মডেল নিজেই সিদ্ধান্ত নেয় কখন সংরক্ষণ/আপডেট/ডিলিট/সার্চ করতে হবে — এখানে কোনো
# কীওয়ার্ড বা প্যাটার্ন ম্যাচিং নেই, শুধু মডেলের টুল-কল অনুযায়ী কাজ করা হয়।
DB_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), "memory.db")
def db_init():
conn = sqlite3.connect(DB_PATH)
conn.execute("""
CREATE TABLE IF NOT EXISTS facts (
id INTEGER PRIMARY KEY AUTOINCREMENT,
topic TEXT NOT NULL,
content TEXT NOT NULL,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL
)
""")
conn.commit()
conn.close()
db_init()
def _now():
return datetime.now().strftime("%Y-%m-%d %H:%M:%S")
def db_find_best_match(topic):
"""topic-এর সাথে সবচেয়ে কাছাকাছি মিল থাকা রেকর্ড খুঁজে বের করে (সাবস্ট্রিং ম্যাচ)।"""
conn = sqlite3.connect(DB_PATH)
conn.row_factory = sqlite3.Row
row = conn.execute(
"SELECT * FROM facts WHERE topic LIKE ? ORDER BY updated_at DESC LIMIT 1",
(f"%{topic}%",)
).fetchone()
conn.close()
return dict(row) if row else None
def tool_save_fact(topic, content):
existing = db_find_best_match(topic)
conn = sqlite3.connect(DB_PATH)
if existing:
conn.execute(
"UPDATE facts SET topic=?, content=?, updated_at=? WHERE id=?",
(topic, content, _now(), existing["id"])
)
conn.commit()
conn.close()
return {"status": "already_existed_so_updated", "topic": topic, "content": content}
conn.execute(
"INSERT INTO facts (topic, content, created_at, updated_at) VALUES (?, ?, ?, ?)",
(topic, content, _now(), _now())
)
conn.commit()
conn.close()
return {"status": "created", "topic": topic, "content": content}
def tool_update_fact(topic, content):
existing = db_find_best_match(topic)
conn = sqlite3.connect(DB_PATH)
if not existing:
conn.execute(
"INSERT INTO facts (topic, content, created_at, updated_at) VALUES (?, ?, ?, ?)",
(topic, content, _now(), _now())
)
conn.commit()
conn.close()
return {"status": "not_found_so_created", "topic": topic, "content": content}
conn.execute(
"UPDATE facts SET topic=?, content=?, updated_at=? WHERE id=?",
(topic, content, _now(), existing["id"])
)
conn.commit()
conn.close()
return {"status": "updated", "old_content": existing["content"], "topic": topic, "content": content}
def tool_delete_fact(topic):
existing = db_find_best_match(topic)
if not existing:
return {"status": "not_found", "topic": topic}
conn = sqlite3.connect(DB_PATH)
conn.execute("DELETE FROM facts WHERE id=?", (existing["id"],))
conn.commit()
conn.close()
return {"status": "deleted", "topic": existing["topic"], "content": existing["content"]}
def _as_list(value):
"""যেকোনো ইনপুটকে (single dict/string বা list) নিরাপদে একটা list-এ রূপান্তর করে।"""
if isinstance(value, list):
return value
if isinstance(value, dict) or isinstance(value, str):
return [value]
return []
def tool_search_facts(query):
conn = sqlite3.connect(DB_PATH)
conn.row_factory = sqlite3.Row
rows = conn.execute(
"SELECT topic, content, updated_at FROM facts WHERE topic LIKE ? OR content LIKE ? ORDER BY updated_at DESC LIMIT 10",
(f"%{query}%", f"%{query}%")
).fetchall()
conn.close()
if not rows:
return {"status": "not_found", "query": query, "results": []}
return {"status": "found", "query": query, "results": [dict(r) for r in rows]}
def tool_list_all_facts():
"""ব্যবহারকারী সম্পর্কে সংরক্ষিত সব তথ্য ফেরত দেয় (ব্যাপক/সামগ্রিক প্রশ্নের জন্য)।"""
conn = sqlite3.connect(DB_PATH)
conn.row_factory = sqlite3.Row
rows = conn.execute(
"SELECT topic, content, updated_at FROM facts ORDER BY updated_at DESC LIMIT 100"
).fetchall()
conn.close()
if not rows:
return {"status": "empty", "results": []}
return {"status": "ok", "count": len(rows), "results": [dict(r) for r in rows]}
TOOLS_SPEC = [
{
"type": "function",
"function": {
"name": "save_facts",
"description": "Save one or more new personal facts about the user. If there are multiple facts, put them all in one call.",
"parameters": {
"type": "object",
"properties": {
"items": {
"type": "array",
"description": "List of facts to save",
"items": {
"type": "object",
"properties": {
"topic": {"type": "string", "description": "Short label for the fact"},
"content": {"type": "string", "description": "The fact itself"}
},
"required": ["topic", "content"]
}
}
},
"required": ["items"]
}
}
},
{
"type": "function",
"function": {
"name": "update_facts",
"description": "Update one or more existing facts. Batch multiple updates into one call.",
"parameters": {
"type": "object",
"properties": {
"items": {
"type": "array",
"description": "List of facts to update",
"items": {
"type": "object",
"properties": {
"topic": {"type": "string", "description": "Label of the fact to update"},
"content": {"type": "string", "description": "The new value"}
},
"required": ["topic", "content"]
}
}
},
"required": ["items"]
}
}
},
{
"type": "function",
"function": {
"name": "delete_facts",
"description": "Delete one or more stored facts. Batch multiple topics into one call.",
"parameters": {
"type": "object",
"properties": {
"topics": {
"type": "array",
"description": "Labels of the facts to delete",
"items": {"type": "string"}
}
},
"required": ["topics"]
}
}
},
{
"type": "function",
"function": {
"name": "search_facts",
"description": "Search stored facts for one or more topics. Batch multiple queries into one call.",
"parameters": {
"type": "object",
"properties": {
"queries": {
"type": "array",
"description": "List of topics/keywords to search for",
"items": {"type": "string"}
}
},
"required": ["queries"]
}
}
},
{
"type": "function",
"function": {
"name": "list_all_facts",
"description": "Return every fact stored about the user. Use for broad questions like 'what do you know about me', or when search_facts finds nothing. No arguments needed.",
"parameters": {
"type": "object",
"properties": {},
"required": []
}
}
}
]
TOOLS_JSON = json.dumps(TOOLS_SPEC, ensure_ascii=False, indent=2)
def exec_save_facts(args):
items = _as_list(args.get("items", []))
results = []
for it in items:
if not isinstance(it, dict):
continue
topic = str(it.get("topic", "")).strip()
content = str(it.get("content", "")).strip()
if topic and content:
results.append(tool_save_fact(topic, content))
return {"status": "ok", "count": len(results), "results": results}
def exec_update_facts(args):
items = _as_list(args.get("items", []))
results = []
for it in items:
if not isinstance(it, dict):
continue
topic = str(it.get("topic", "")).strip()
content = str(it.get("content", "")).strip()
if topic and content:
results.append(tool_update_fact(topic, content))
return {"status": "ok", "count": len(results), "results": results}
def exec_delete_facts(args):
topics = _as_list(args.get("topics", []))
results = []
for t in topics:
topic = t.get("topic") if isinstance(t, dict) else t
topic = str(topic or "").strip()
if topic:
results.append(tool_delete_fact(topic))
return {"status": "ok", "count": len(results), "results": results}
def exec_search_facts(args):
queries = _as_list(args.get("queries", []))
results_by_query = {}
for q in queries:
q = str(q or "").strip()
if q:
results_by_query[q] = tool_search_facts(q)
return {"status": "ok", "results_by_query": results_by_query}
TOOL_EXECUTORS = {
"save_facts": exec_save_facts,
"update_facts": exec_update_facts,
"delete_facts": exec_delete_facts,
"search_facts": exec_search_facts,
"list_all_facts": lambda args: tool_list_all_facts(),
# নিচেরগুলো ব্যাকওয়ার্ড-কম্প্যাটিবিলিটির জন্য — মডেল যদি কখনো একবচন নাম দিয়ে কল করে
# (একটা মাত্র আইটেম নিয়ে) সেটাও যেন কাজ করে, ভুল হলে পুরো উত্তর যেন ব্যর্থ না হয়।
"save_fact": lambda args: exec_save_facts({"items": [args]}),
"update_fact": lambda args: exec_update_facts({"items": [args]}),
"delete_fact": lambda args: exec_delete_facts({"topics": [args.get("topic", "")]}),
}
def execute_tool_call(name, arguments):
executor = TOOL_EXECUTORS.get(name)
if not executor:
return {"error": f"unknown tool: {name}"}
try:
return executor(arguments or {})
except Exception as e:
return {"error": str(e)}
TOOL_CALL_RE = re.compile(r"\s*(\{.*?\})\s*", re.DOTALL)
SYSTEM_PROMPT = f"""You are a helpful assistant with a personal facts database about the user.
# Tools
{TOOLS_JSON}
Rules:
- New personal info -> save_facts (batch ALL items into one call, not one call per fact).
- Info to change -> update_facts (batch).
- Info to remove -> delete_facts (batch).
- Specific question -> search_facts (batch all queries into one call).
- Broad question ("what do you know about me") or search_facts finds nothing -> list_all_facts.
- No database need -> just answer directly, no tool call.
- Always merge multiple items into ONE tool_call using arrays. Only use separate blocks for genuinely different tools in the same turn.
To call a tool, output exactly this format (nothing else):
{{"name": "...", "arguments": {{...}}}}
Example (user gives several facts at once):
{{"name": "save_facts", "arguments": {{"items": [
{{"topic": "name", "content": "Mukul"}},
{{"topic": "age", "content": "35"}}
]}}}}
After a tool call you will receive results in tags. Then give a short, natural, human-sounding final answer, in the same language the user wrote in. Never show JSON, tags, function names, or the word "database" in your final answer."""
def build_base_prompt(history, user_prompt):
"""সিস্টেম প্রম্পট + কথোপকথনের হিস্টরি + বর্তমান প্রশ্ন দিয়ে সম্পূর্ণ প্রম্পট তৈরি করে।"""
prompt = f"<|im_start|>system\n{SYSTEM_PROMPT}<|im_end|>\n"
for turn in history:
h_user = str(turn.get('user', '')).strip()
h_assistant = str(turn.get('assistant', '')).strip()
if not h_user:
continue
prompt += f"<|im_start|>user\n{h_user}<|im_end|>\n<|im_start|>assistant\n\n\n\n\n{h_assistant}<|im_end|>\n"
prompt += f"<|im_start|>user\n{user_prompt}<|im_end|>\n<|im_start|>assistant\n\n\n\n\n"
return prompt
# --- ৩. HTML টেমপ্লেট ---
HTML_TEMPLATE = """
Bonsai 8B চ্যাট (Hugging Face Space)
🌳 Bonsai 8B (১-বিট) চ্যাট
Hugging Face Space • CPU Inference • মডেল সাইজ: ~১.১৫ GB (Q1_0, 1-bit)
➕ নতুন চ্যাট
👋 হ্যালো! আমি Bonsai 8B। আপনি কী জানতে চান?
✅ প্রস্তুত
"""
# --- ৪. Flask রাউট ---
@app.route('/')
def index():
return render_template_string(HTML_TEMPLATE)
@app.route('/chat', methods=['POST'])
def chat():
data = request.get_json()
user_prompt = data.get('prompt', '')
# ফ্রন্টএন্ড থেকে পাঠানো সর্বশেষ (সর্বোচ্চ ৩টা) প্রশ্ন-উত্তরের হিস্টরি
# প্রতিটা আইটেম: {"user": "...", "assistant": "..."}
history = data.get('history', [])
if not user_prompt:
return {"error": "কোনো প্রশ্ন নেই"}
if llm is None:
return {"error": "মডেল লোড হয়নি। সার্ভার লগ চেক করুন।"}
# শুধু সর্বশেষ ৩টা এক্সচেঞ্জ ব্যবহার করা হচ্ছে (কনটেক্সট উইন্ডো সীমিত রাখতে)
if isinstance(history, list):
history = history[-3:]
else:
history = []
# হিস্টরি + সিস্টেম প্রম্পট (টুল সংজ্ঞাসহ) দিয়ে প্রম্পট তৈরি
prompt = build_base_prompt(history, user_prompt)
def generate():
start_time = time.time()
start_str = time.strftime('%H:%M:%S', time.localtime(start_time))
first_token_time = None
try:
stream1 = llm(
prompt,
max_tokens=900,
temperature=0.1,
top_p=0.2,
top_k=40,
repeat_penalty=1.1,
stop=["<|im_end|>", "user:", "User:"],
stream=True
)
# প্রথম কয়েক অক্ষর বাফার করে দেখা হচ্ছে মডেল টুল কল করছে নাকি সরাসরি উত্তর দিচ্ছে —
# এটা কোনো কীওয়ার্ড/প্যাটার্ন ম্যাচিং না, মডেল নিজেই ট্যাগ দিয়ে
# শুরু করে কিনা সেটা শুধু চেক করা হচ্ছে (এটাই মডেলের নিজস্ব ফাংশন-কলিং ফরম্যাট)।
head_buffer = ""
head_decided = False
is_tool_mode = False
tool_call_text = ""
TOOL_TAG = ""
for chunk in stream1:
token_text = chunk["choices"][0]["text"]
if not token_text:
continue
if first_token_time is None:
first_token_time = time.time() - start_time
if not head_decided:
head_buffer += token_text
stripped = head_buffer.lstrip()
if not stripped:
continue
if stripped.startswith(TOOL_TAG):
head_decided = True
is_tool_mode = True
tool_call_text = head_buffer
yield "[[TOOL_START]]"
elif len(stripped) >= len(TOOL_TAG) or not TOOL_TAG.startswith(stripped):
head_decided = True
is_tool_mode = False
yield head_buffer
continue
if is_tool_mode:
tool_call_text += token_text
else:
yield token_text
if not head_decided:
stripped = head_buffer.lstrip()
if stripped.startswith(TOOL_TAG):
is_tool_mode = True
tool_call_text = head_buffer
yield "[[TOOL_START]]"
elif head_buffer:
yield head_buffer
if is_tool_mode:
# মডেলের টুল কল(গুলো) পার্স ও এক্সিকিউট করা
tool_calls = TOOL_CALL_RE.findall(tool_call_text)
tool_response_blocks = ""
for tc_raw in tool_calls:
try:
tc = json.loads(tc_raw)
name = tc.get("name", "")
args = tc.get("arguments", {}) or {}
result = execute_tool_call(name, args)
except Exception as e:
result = {"error": f"failed to parse tool call: {str(e)}"}
tool_response_blocks += (
"<|im_start|>tool\n\n"
f"{json.dumps(result, ensure_ascii=False)}\n"
"<|im_end|>\n"
)
yield "[[TOOL_DONE]]"
follow_prompt = (
prompt + tool_call_text.strip() + "<|im_end|>\n"
+ tool_response_blocks
+ "<|im_start|>assistant\n\n\n\n\n"
)
stream2 = llm(
follow_prompt,
max_tokens=100,
temperature=0.1,
top_p=0.2,
top_k=20,
repeat_penalty=1.4,
stop=["<|im_end|>", "", "user:", "User:"],
stream=True
)
for chunk in stream2:
token_text = chunk["choices"][0]["text"]
if token_text:
yield token_text
except Exception as e:
print(f"❌ জেনারেশন ত্রুটি: {e}")
yield f"\n⚠️ মডেল ত্রুটি: {str(e)}"
finally:
end_time = time.time()
end_str = time.strftime('%H:%M:%S', time.localtime(end_time))
elapsed = round(end_time - start_time, 2)
first_token = round(first_token_time, 2) if first_token_time else 0
print(f"⏱️ প্রথম টোকেন: {first_token} সেকেন্ড | মোট সময়: {elapsed} সেকেন্ড")
# স্ট্রিমের একদম শেষে টাইমিং তথ্য পাঠানো
meta = {
"start": start_str,
"end": end_str,
"elapsed": elapsed,
"first_token_time": first_token
}
yield f"\n[[META]]{json.dumps(meta, ensure_ascii=False)}"
return Response(stream_with_context(generate()), mimetype='text/plain')
# --- ৫. সার্ভার চালানো ---
if __name__ == '__main__':
app.run(host='0.0.0.0', port=7860)