|
|
#!/usr/bin/env bash |
|
|
set -euo pipefail |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
ROOT_DIR="$(pwd)" |
|
|
VENV_DIR="$ROOT_DIR/.venv" |
|
|
|
|
|
echo "==> Preparing virtualenv..." |
|
|
if ! command -v python3 >/dev/null 2>&1; then |
|
|
echo "Python3 is required. Install Python 3.10+ and retry." |
|
|
exit 1 |
|
|
fi |
|
|
|
|
|
python3 -m venv "$VENV_DIR" |
|
|
|
|
|
source "$VENV_DIR/bin/activate" |
|
|
|
|
|
pip install --upgrade pip |
|
|
|
|
|
cat > requirements.txt <<'PYREQ' |
|
|
fastapi==0.95.2 |
|
|
uvicorn[standard]==0.21.1 |
|
|
httpx==0.24.1 |
|
|
python-dotenv==1.0.0 |
|
|
python-multipart==0.0.6 |
|
|
sqlalchemy==2.0.19 |
|
|
pydantic==1.10.12 |
|
|
PYREQ |
|
|
|
|
|
echo "==> Installing Python dependencies (lightweight)..." |
|
|
pip install -r requirements.txt |
|
|
|
|
|
|
|
|
cat > app.py <<'PYAPP' |
|
|
import os |
|
|
import json |
|
|
import sqlite3 |
|
|
import urllib.parse |
|
|
from typing import Optional, List, Dict |
|
|
from fastapi import FastAPI, Request, HTTPException, UploadFile, File, Form |
|
|
from pydantic import BaseModel |
|
|
import httpx |
|
|
from starlette.responses import PlainTextResponse, HTMLResponse, Response, FileResponse |
|
|
|
|
|
|
|
|
HF_API_TOKEN = os.getenv("HF_API_TOKEN", "") |
|
|
HF_MODEL_ID = os.getenv("HF_MODEL_ID", "gpt2") |
|
|
COGNITO_API_KEY = os.getenv("COGNITO_API_KEY", "cognito_default_key_please_change") |
|
|
ALLOW_FETCH = os.getenv("ALLOW_FETCH", "false").lower() == "true" |
|
|
FETCH_BEARER_TOKEN = os.getenv("FETCH_BEARER_TOKEN", "") |
|
|
MODERATION_BLOCKLIST = [s.strip().lower() for s in os.getenv("MODERATION_BLOCKLIST", "hack,steal,illegal").split(",") if s.strip()] |
|
|
|
|
|
DB_PATH = os.path.join(os.getcwd(), "cognito_sessions.db") |
|
|
UPLOADS_DIR = os.path.join(os.getcwd(), "uploads") |
|
|
os.makedirs(UPLOADS_DIR, exist_ok=True) |
|
|
|
|
|
app = FastAPI(title="Cognito (Minimal)", docs_url="/docs") |
|
|
|
|
|
|
|
|
def init_db(): |
|
|
conn = sqlite3.connect(DB_PATH) |
|
|
cur = conn.cursor() |
|
|
cur.execute(""" |
|
|
CREATE TABLE IF NOT EXISTS sessions ( |
|
|
session_id TEXT PRIMARY KEY, |
|
|
messages TEXT |
|
|
) |
|
|
""") |
|
|
conn.commit() |
|
|
conn.close() |
|
|
|
|
|
def get_session_messages(session_id: str): |
|
|
conn = sqlite3.connect(DB_PATH) |
|
|
cur = conn.cursor() |
|
|
cur.execute("SELECT messages FROM sessions WHERE session_id = ?", (session_id,)) |
|
|
row = cur.fetchone() |
|
|
conn.close() |
|
|
if row: |
|
|
return json.loads(row[0]) |
|
|
return [] |
|
|
|
|
|
def save_session_messages(session_id: str, messages): |
|
|
conn = sqlite3.connect(DB_PATH) |
|
|
cur = conn.cursor() |
|
|
cur.execute("INSERT OR REPLACE INTO sessions (session_id, messages) VALUES (?, ?)", (session_id, json.dumps(messages))) |
|
|
conn.commit() |
|
|
conn.close() |
|
|
|
|
|
init_db() |
|
|
|
|
|
|
|
|
class ChatRequest(BaseModel): |
|
|
session_id: Optional[str] = None |
|
|
messages: List[Dict] |
|
|
fetch_url: Optional[str] = None |
|
|
|
|
|
|
|
|
def moderate_messages(messages): |
|
|
txt = " ".join(m.get("content","") for m in messages).lower() |
|
|
for bad in MODERATION_BLOCKLIST: |
|
|
if bad and bad in txt: |
|
|
return False, f"Message blocked by moderation: found banned word '{bad}'" |
|
|
return True, "" |
|
|
|
|
|
|
|
|
def build_prompt(messages, uploaded_texts=None): |
|
|
prompt = "" |
|
|
if uploaded_texts: |
|
|
prompt += "Context documents:\n" |
|
|
for i, t in enumerate(uploaded_texts): |
|
|
prompt += f"[DOC {i+1}]\n{t}\n\n" |
|
|
prompt += "---\n" |
|
|
for m in messages: |
|
|
role = m.get("role","user") |
|
|
content = m.get("content","") |
|
|
if role == "system": |
|
|
prompt += f"[SYSTEM] {content}\n" |
|
|
elif role == "user": |
|
|
prompt += f"User: {content}\n" |
|
|
else: |
|
|
prompt += f"Assistant: {content}\n" |
|
|
prompt += "\nAssistant:" |
|
|
return prompt |
|
|
|
|
|
|
|
|
@app.post("/upload") |
|
|
async def upload_file(session_id: Optional[str] = Form(None), file: UploadFile = File(...)): |
|
|
|
|
|
content = await file.read() |
|
|
fname = file.filename |
|
|
safe_name = fname.replace("..","_").replace("/","_") |
|
|
path = os.path.join(UPLOADS_DIR, safe_name) |
|
|
with open(path, "wb") as f: |
|
|
f.write(content) |
|
|
return {"ok": True, "path": path} |
|
|
|
|
|
|
|
|
@app.get("/cognito-widget.js") |
|
|
def widget_js(): |
|
|
|
|
|
js = """ |
|
|
(function () { |
|
|
if (window.CognitoWidgetInitialized) return; |
|
|
window.CognitoWidgetInitialized = true; |
|
|
const CONFIG = { apiBase: window.COGNITO_API_BASE || "http://localhost:8000", apiKey: window.COGNITO_API_KEY || null, title: "Cognito" }; |
|
|
const css = `.cognito-chat-btn { position: fixed; right: 20px; bottom: 20px; z-index: 100000; background: #0b5fff; color: #fff; border-radius: 28px; padding: 12px 16px; cursor: pointer; } .cognito-panel { position: fixed; right: 20px; bottom: 80px; width: 360px; max-width: 90vw; z-index: 100000; border-radius: 12px; overflow: hidden; background: #fff; display:none; flex-direction:column } .cognito-header{ padding:12px; background:#0b5fff; color:#fff } .cognito-body{ padding:12px; height:300px; overflow:auto; background:#f7f7fb } .cognito-input-row{ display:flex; padding:8px; border-top:1px solid #eee } .cognito-input{ flex:1; padding:8px 10px; border-radius:8px; border:1px solid #ddd } .cognito-send{ margin-left:8px; padding:8px 12px; border-radius:8px; border:none; background:#0b5fff; color:#fff } .cognito-msg{ margin-bottom:8px; padding:8px 10px; border-radius:8px; max-width:85% } .cognito-msg.user{ background:#0b5fff; color:#fff; margin-left:auto } .cognito-msg.bot{ background:#fff; color:#111; border:1px solid #eee }`; |
|
|
const styleEl=document.createElement("style"); styleEl.innerHTML=css; document.head.appendChild(styleEl); |
|
|
const btn=document.createElement("button"); btn.className='cognito-chat-btn'; btn.innerText=CONFIG.title; document.body.appendChild(btn); |
|
|
const panel=document.createElement("div"); panel.className='cognito-panel'; panel.innerHTML='<div class=\"cognito-header\">Cognito</div><div class=\"cognito-body\"><div class=\"cognito-msg bot\">Hello — I am Cognito.</div></div><div class=\"cognito-input-row\"><input class=\"cognito-input\" placeholder=\"Ask Cognito...\" /><button class=\"cognito-send\">Send</button></div>'; document.body.appendChild(panel); |
|
|
btn.addEventListener('click', ()=>{ panel.style.display = panel.style.display === 'flex' ? 'none' : 'flex'; panel.style.flexDirection='column'; }); |
|
|
const bodyEl=panel.querySelector('.cognito-body'); const inputEl=panel.querySelector('.cognito-input'); const sendBtn=panel.querySelector('.cognito-send'); |
|
|
function appendMessage(text, who){ const m=document.createElement('div'); m.className='cognito-msg '+(who==='user'?'user':'bot'); m.innerText=text; bodyEl.appendChild(m); bodyEl.scrollTop = bodyEl.scrollHeight; } |
|
|
async function sendMessage(text){ appendMessage(text,'user'); inputEl.value=''; const payload={ messages: [{role:'user', content:text}], session_id: null }; const headers={'Content-Type':'application/json'}; if(CONFIG.apiKey) headers['x-api-key']=CONFIG.apiKey; try{ const res = await fetch(CONFIG.apiBase + '/chat',{ method:'POST', headers, body: JSON.stringify(payload) }); if(!res.ok){ appendMessage('Error: '+res.status+' '+await res.text(),'bot'); return; } const data = await res.json(); appendMessage(data.reply || JSON.stringify(data), 'bot'); } catch(err){ appendMessage('Network error: '+err.message,'bot'); } } |
|
|
sendBtn.addEventListener('click', ()=>{ const v=inputEl.value.trim(); if(!v) return; sendMessage(v); }); |
|
|
inputEl.addEventListener('keydown', (e)=>{ if(e.key==='Enter'){ e.preventDefault(); sendBtn.click(); } }); |
|
|
window.CognitoWidget = { send: sendMessage, open: () => { panel.style.display='flex'; }, close: () => { panel.style.display='none'; } }; |
|
|
})(); |
|
|
""" |
|
|
return Response(content=js, media_type="application/javascript") |
|
|
|
|
|
|
|
|
@app.get("/") |
|
|
def index(): |
|
|
content = f"<html><body><h3>Cognito (minimal)</h3><p>Use the widget by embedding <code><script src=\"/cognito-widget.js\"></script></code> and set <code>window.COGNITO_API_BASE</code> & <code>window.COGNITO_API_KEY</code>.</p><p>HF_API_TOKEN set: {bool(HF_API_TOKEN)}</p></body></html>" |
|
|
return HTMLResponse(content=content) |
|
|
|
|
|
|
|
|
async def fetch_url_with_bearer(url: str): |
|
|
if not ALLOW_FETCH: |
|
|
raise HTTPException(status_code=403, detail="Server is not configured to fetch external pages.") |
|
|
parsed = urllib.parse.urlparse(url) |
|
|
if parsed.scheme not in ("http","https"): |
|
|
raise HTTPException(status_code=400, detail="Invalid URL scheme.") |
|
|
headers = {"User-Agent": "CognitoFetcher/1.0"} |
|
|
if FETCH_BEARER_TOKEN: |
|
|
headers["Authorization"] = f"Bearer {FETCH_BEARER_TOKEN}" |
|
|
async with httpx.AsyncClient(timeout=20.0) as client: |
|
|
r = await client.get(url, headers=headers) |
|
|
if r.status_code >= 400: |
|
|
raise HTTPException(status_code=502, detail=f"Upstream fetch failed: {r.status_code}") |
|
|
|
|
|
return r.text[:20000] |
|
|
|
|
|
|
|
|
def call_hf_inference(prompt: str): |
|
|
if not HF_API_TOKEN: |
|
|
|
|
|
return "Cognito (offline mode): I received your message. Provide HF_API_TOKEN to enable real model responses." |
|
|
url = f"https://api-inference.huggingface.co/models/{HF_MODEL_ID}" |
|
|
headers = {"Authorization": f"Bearer {HF_API_TOKEN}", "Content-Type": "application/json"} |
|
|
payload = {"inputs": prompt, "options": {"wait_for_model": True}} |
|
|
try: |
|
|
with httpx.Client(timeout=60.0) as client: |
|
|
r = client.post(url, headers=headers, json=payload) |
|
|
r.raise_for_status() |
|
|
out = r.json() |
|
|
|
|
|
if isinstance(out, list) and len(out) > 0 and isinstance(out[0], dict): |
|
|
return out[0].get("generated_text") or str(out) |
|
|
if isinstance(out, dict): |
|
|
return out.get("generated_text") or str(out) |
|
|
return str(out) |
|
|
except Exception as e: |
|
|
return f"Cognito: error calling HF inference: {str(e)}" |
|
|
|
|
|
|
|
|
@app.post("/chat") |
|
|
async def chat(req: ChatRequest, request: Request): |
|
|
|
|
|
header_key = request.headers.get("x-api-key", "") |
|
|
if header_key != COGNITO_API_KEY: |
|
|
raise HTTPException(status_code=401, detail="Invalid API key.") |
|
|
|
|
|
|
|
|
ok, reason = moderate_messages(req.messages) |
|
|
if not ok: |
|
|
raise HTTPException(status_code=400, detail=reason) |
|
|
|
|
|
uploaded_texts = [] |
|
|
|
|
|
if req.session_id: |
|
|
|
|
|
for fname in os.listdir(UPLOADS_DIR): |
|
|
if fname.startswith(req.session_id): |
|
|
path = os.path.join(UPLOADS_DIR, fname) |
|
|
try: |
|
|
with open(path, "r", encoding="utf-8", errors="replace") as f: |
|
|
uploaded_texts.append(f.read()[:5000]) |
|
|
except Exception: |
|
|
pass |
|
|
|
|
|
|
|
|
fetched = "" |
|
|
if req.fetch_url: |
|
|
fetched = await fetch_url_with_bearer(req.fetch_url) |
|
|
|
|
|
prompt = build_prompt(req.messages, uploaded_texts + ([fetched] if fetched else [])) |
|
|
reply = call_hf_inference(prompt) |
|
|
|
|
|
|
|
|
sess_id = req.session_id or "anon" |
|
|
prev = get_session_messages(sess_id) |
|
|
prev.append({"role":"user","content": req.messages[-1].get("content","") if req.messages else ""}) |
|
|
prev.append({"role":"assistant","content": reply}) |
|
|
|
|
|
if len(prev) > 50: |
|
|
prev = prev[-50:] |
|
|
save_session_messages(sess_id, prev) |
|
|
|
|
|
return {"reply": reply} |
|
|
|
|
|
|
|
|
@app.get("/health") |
|
|
def health(): |
|
|
return {"status": "ok", "hf_token": bool(HF_API_TOKEN), "allow_fetch": ALLOW_FETCH} |
|
|
PYAPP |
|
|
|
|
|
|
|
|
cat > cognito-widget.js <<'WIDGET' |
|
|
/* This file is also available at /cognito-widget.js when server is running. |
|
|
The hosted widget expects window.COGNITO_API_BASE and window.COGNITO_API_KEY on the host page. */ |
|
|
(function () { |
|
|
if (window.CognitoWidgetInitialized) return; |
|
|
window.CognitoWidgetInitialized = true; |
|
|
const CONFIG = { apiBase: window.COGNITO_API_BASE || "http://localhost:8000", apiKey: window.COGNITO_API_KEY || null, title: "Cognito" }; |
|
|
const css = `.cognito-chat-btn { position: fixed; right: 20px; bottom: 20px; z-index: 100000; background: |
|
|
const styleEl=document.createElement("style"); styleEl.innerHTML=css; document.head.appendChild(styleEl); |
|
|
const btn=document.createElement("button"); btn.className='cognito-chat-btn'; btn.innerText=CONFIG.title; document.body.appendChild(btn); |
|
|
const panel=document.createElement("div"); panel.className='cognito-panel'; panel.innerHTML='<div class="cognito-header">Cognito</div><div class="cognito-body"><div class="cognito-msg bot">Hello — I am Cognito.</div></div><div class="cognito-input-row"><input class="cognito-input" placeholder="Ask Cognito..." /><button class="cognito-send">Send</button></div>'; document.body.appendChild(panel); |
|
|
btn.addEventListener('click', ()=>{ panel.style.display = panel.style.display === 'flex' ? 'none' : 'flex'; panel.style.flexDirection='column'; }); |
|
|
const bodyEl=panel.querySelector('.cognito-body'); const inputEl=panel.querySelector('.cognito-input'); const sendBtn=panel.querySelector('.cognito-send'); |
|
|
function appendMessage(text, who){ const m=document.createElement('div'); m.className='cognito-msg '+(who==='user'?'user':'bot'); m.innerText=text; bodyEl.appendChild(m); bodyEl.scrollTop = bodyEl.scrollHeight; } |
|
|
async function sendMessage(text){ appendMessage(text,'user'); inputEl.value=''; const payload={ messages: [{role:'user', content:text}], session_id: null }; const headers={'Content-Type':'application/json'}; if(CONFIG.apiKey) headers['x-api-key']=CONFIG.apiKey; try{ const res = await fetch(CONFIG.apiBase + '/chat',{ method:'POST', headers, body: JSON.stringify(payload) }); if(!res.ok){ appendMessage('Error: '+res.status+' '+await res.text(),'bot'); return; } const data = await res.json(); appendMessage(data.reply || JSON.stringify(data), 'bot'); } catch(err){ appendMessage('Network error: '+err.message,'bot'); } } |
|
|
sendBtn.addEventListener('click', ()=>{ const v=inputEl.value.trim(); if(!v) return; sendMessage(v); }); |
|
|
inputEl.addEventListener('keydown', (e)=>{ if(e.key==='Enter'){ e.preventDefault(); sendBtn.click(); } }); |
|
|
window.CognitoWidget = { send: sendMessage, open: () => { panel.style.display='flex'; }, close: () => { panel.style.display='none'; } }; |
|
|
})(); |
|
|
WIDGET |
|
|
|
|
|
|
|
|
cat > .env <<'ENV' |
|
|
|
|
|
|
|
|
HF_API_TOKEN="" |
|
|
HF_MODEL_ID="gpt2" |
|
|
COGNITO_API_KEY="cognito_default_key_please_change" |
|
|
ALLOW_FETCH="false" |
|
|
FETCH_BEARER_TOKEN="" |
|
|
ENV |
|
|
|
|
|
echo "==> Starting server (virtualenv active)." |
|
|
echo "API key (for widget) default: cognito_default_key_please_change" |
|
|
echo "If you have a Hugging Face token, export HF_API_TOKEN before running this script for real model responses." |
|
|
echo "Example to run with token: HF_API_TOKEN='hf_xxx' ./setup_and_run.sh" |
|
|
|
|
|
|
|
|
|
|
|
uvicorn app:app --host 0.0.0.0 --port 8000 --reload |
|
|
|