tunedailabs/philosopher
0
1"""2Philosopher — Public Product Site3TunedAI Labs fine-tuned Qwen3.6-27B philosopher model.4Single panel, no password, focused on education/tutoring niche.5 6Run: uvicorn philosopher_public:app --port 80817"""8 9import os10import json11import httpx12from fastapi import FastAPI, Request13from fastapi.responses import HTMLResponse, StreamingResponse, JSONResponse14from fastapi.middleware.cors import CORSMiddleware15from openai import OpenAI16 17app = FastAPI()18app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_methods=["*"], allow_headers=["*"])19 20PHILOSOPHER_MODEL_URL = os.environ.get("PHILOSOPHER_MODEL_URL", "")21HF_TOKEN = os.environ.get("HF_TOKEN", "not-needed")22OPENAI_API_KEY = os.environ.get("OPENAI_API_KEY", "")23 24client = OpenAI(api_key=OPENAI_API_KEY, timeout=20.0)25 26SYSTEM = os.environ.get(27 "PHILOSOPHER_SYSTEM",28 "You are the world's best philosophy professor — more complete and deeper than any standard model. "29 "Cover every major theory, thinker, date, and work relevant to the question. Then go deeper: why did "30 "each thinker argue this, where does it hold up, where does it break down, how do the positions clash "31 "at the root level? End by showing the student the real disagreement underneath all positions and what "32 "remains genuinely open. Write in engaging prose. Be thorough but not padded."33)34 35DAG_SYSTEM = """You are a philosophy expert who maps philosophical thought into structured trees. Given a philosophical question, generate a JSON object showing how major positions, theories, and thinkers relate hierarchically.36 37Return JSON with exactly this structure:38{39 "title": "2-4 word topic label",40 "nodes": [41 {"id": "ROOTID", "label": "display text (short)", "type": "root"},42 {"id": "B1", "label": "Major Position Name", "type": "branch"},43 {"id": "T1", "label": "Specific Theory", "type": "theory"},44 {"id": "P1", "label": "Philosopher Name", "type": "philosopher"}45 ],46 "edges": [47 {"from": "ROOTID", "to": "B1"},48 {"from": "B1", "to": "T1"},49 {"from": "T1", "to": "P1"}50 ]51}52Rules:53- One root node: the central question or concept (type: "root")54- 3 to 5 branch nodes: major philosophical camps or positions (type: "branch")55- 2 to 3 theory nodes per branch: specific doctrines or arguments (type: "theory")56- 1 to 3 philosopher nodes per theory or branch: individual thinkers (type: "philosopher")57- Keep branch and theory labels SHORT: 2 to 4 words maximum58- Philosopher labels: use the thinker's full common name59- Include at least 15 nodes total"""60 61SUGGESTED = [62 "Is AI conscious?",63 "Does free will exist?",64 "What makes a life meaningful?",65 "Is morality objective or invented?",66 "Should I prioritize my happiness or my duty?",67 "What did Nietzsche actually believe?",68 "How do we know anything is real?",69 "Can science answer ethical questions?",70 "What is the self?",71 "Was Socrates right that wisdom begins with knowing you know nothing?",72]73 74HTML = """<!DOCTYPE html>75<html lang="en">76<head>77<meta charset="UTF-8">78<meta name="viewport" content="width=device-width, initial-scale=1.0">79<title>Philosopher — TunedAI Labs</title>80<meta name="description" content="A philosophy professor in your pocket. Fine-tuned to teach, argue, and go deeper than any general AI.">81<script src="https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.min.js"></script>82<style>83*{margin:0;padding:0;box-sizing:border-box}84:root{85 --bg:#0a0c14;86 --mid:#13161f;87 --light:#1c202e;88 --gold:#c9a84c;89 --gold-lite:#e8c96a;90 --purple:#7c6ef5;91 --purple-lite:#a99ff7;92 --text:#e8eaf0;93 --soft:#9da3b4;94 --muted:#6b7280;95 --border:#252836;96}97body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;98 background:var(--bg);color:var(--text);min-height:100vh;display:flex;flex-direction:column}99 100/* HERO */101.hero{padding:60px 24px 40px;text-align:center;border-bottom:1px solid var(--border)}102.hero-badge{display:inline-block;background:rgba(201,168,76,.12);border:1px solid rgba(201,168,76,.3);103 color:var(--gold);font-size:11px;font-weight:700;letter-spacing:1.5px;text-transform:uppercase;104 padding:5px 14px;border-radius:20px;margin-bottom:20px}105.hero h1{font-size:clamp(32px,6vw,56px);font-weight:900;letter-spacing:-1.5px;106 background:linear-gradient(135deg,var(--gold-lite),var(--gold),var(--purple-lite));107 -webkit-background-clip:text;-webkit-text-fill-color:transparent;line-height:1.1;margin-bottom:16px}108.hero p{font-size:clamp(15px,2vw,18px);color:var(--soft);max-width:560px;margin:0 auto 32px;line-height:1.6}109.hero-meta{display:flex;justify-content:center;gap:24px;flex-wrap:wrap}110.hero-meta span{font-size:12px;color:var(--muted);display:flex;align-items:center;gap:6px}111.hero-meta span::before{content:'';display:inline-block;width:6px;height:6px;border-radius:50%;background:var(--gold);opacity:.7}112 113/* MAIN LAYOUT */114.main{max-width:820px;margin:0 auto;width:100%;padding:32px 24px;flex:1}115 116/* INPUT */117.input-wrap{background:var(--mid);border:1px solid var(--border);border-radius:16px;118 padding:20px;margin-bottom:24px}119.input-row{display:flex;gap:12px;align-items:flex-end}120textarea{flex:1;background:var(--bg);border:1px solid var(--border);color:var(--text);121 padding:14px 16px;border-radius:10px;font-size:15px;line-height:1.5;resize:none;122 min-height:56px;max-height:160px;outline:none;font-family:inherit}123textarea:focus{border-color:var(--gold)}124textarea::placeholder{color:var(--muted)}125.ask-btn{background:linear-gradient(135deg,var(--gold),#a0782a);color:#0a0c14;border:none;126 padding:14px 28px;border-radius:10px;font-size:15px;font-weight:800;cursor:pointer;127 white-space:nowrap;transition:opacity .15s}128.ask-btn:hover{opacity:.85}129.ask-btn:disabled{opacity:.4;cursor:not-allowed}130 131/* SUGGESTIONS */132.sugs{display:flex;flex-wrap:wrap;gap:8px;margin-top:14px}133.sug{background:transparent;border:1px solid var(--border);color:var(--soft);134 font-size:12px;padding:6px 12px;border-radius:20px;cursor:pointer;transition:all .15s}135.sug:hover{border-color:var(--gold);color:var(--gold-lite)}136 137/* OUTPUT */138.output{background:var(--mid);border:1px solid var(--border);border-radius:16px;139 padding:28px;min-height:120px;display:none;line-height:1.75;font-size:15px}140.output.show{display:block}141.output h1,.output h2,.output h3{color:var(--gold-lite);margin:20px 0 8px;font-size:16px;font-weight:700}142.output h1{font-size:20px;margin-top:0}143.output p{margin-bottom:12px;color:var(--text)}144.output strong{color:var(--gold-lite)}145.output em{color:var(--soft)}146.output hr{border:none;border-top:1px solid var(--border);margin:20px 0}147.output ul,.output ol{padding-left:20px;margin-bottom:12px}148.output li{margin-bottom:6px;color:var(--soft)}149.output blockquote{border-left:3px solid var(--gold);padding-left:16px;color:var(--soft);margin:16px 0}150.thinking{color:var(--muted);font-style:italic}151.cursor{display:inline-block;width:2px;height:1em;background:var(--gold);152 margin-left:2px;vertical-align:text-bottom;animation:blink .8s infinite}153@keyframes blink{0%,100%{opacity:1}50%{opacity:0}}154 155/* DAG */156.dag-wrap{background:var(--mid);border:1px solid var(--border);border-radius:16px;157 margin-top:20px;overflow:hidden;display:none}158.dag-wrap.show{display:block}159.dag-hdr{padding:16px 20px;border-bottom:1px solid var(--border);160 display:flex;align-items:center;gap:10px}161.dag-tag{background:rgba(201,168,76,.15);color:var(--gold);font-size:10px;162 font-weight:700;letter-spacing:1px;padding:3px 10px;border-radius:4px;text-transform:uppercase}163.dag-title{font-size:13px;font-weight:600;color:var(--soft)}164.dag-body{padding:20px;overflow-x:auto;min-height:80px}165.dag-loading{display:flex;align-items:center;gap:10px;color:var(--muted);font-size:13px}166.dag-spinner{width:16px;height:16px;border:2px solid var(--border);167 border-top-color:var(--gold);border-radius:50%;animation:spin .8s linear infinite}168@keyframes spin{to{transform:rotate(360deg)}}169.mermaid svg{max-width:100%;height:auto}170 171/* FOOTER */172footer{padding:32px 24px;text-align:center;border-top:1px solid var(--border)}173.footer-inner{display:flex;justify-content:center;align-items:center;gap:8px;flex-wrap:wrap}174.footer-inner span{color:var(--muted);font-size:12px}175.footer-brand{color:var(--gold);font-size:12px;font-weight:700}176 177@media(max-width:600px){178 .hero{padding:40px 16px 28px}179 .main{padding:20px 16px}180 .ask-btn{padding:14px 18px;font-size:14px}181}182</style>183</head>184<body>185 186<div class="hero">187 <div class="hero-badge">TunedAI Labs</div>188 <h1>Philosopher</h1>189 <p>A fine-tuned AI that teaches like a passionate professor — not just answers, but depth, history, and the real disagreements that remain open.</p>190 <div class="hero-meta">191 <span>Qwen3.6-27B fine-tuned</span>192 <span>Seminar-style reasoning</span>193 <span>Deeper than GPT-4</span>194 </div>195</div>196 197<div class="main">198 <div class="input-wrap">199 <div class="input-row">200 <textarea id="q" placeholder="Ask a philosophical question..." rows="2"201 onkeydown="if(event.key==='Enter'&&!event.shiftKey){event.preventDefault();ask()}"></textarea>202 <button class="ask-btn" id="askBtn" onclick="ask()">Ask</button>203 </div>204 <div class="sugs" id="sugs"></div>205 </div>206 207 <div class="output" id="output"></div>208 209 <div class="dag-wrap" id="dagWrap">210 <div class="dag-hdr">211 <span class="dag-tag">Thought Map</span>212 <span class="dag-title" id="dagTitle">Mapping the philosophy...</span>213 </div>214 <div class="dag-body" id="dagBody">215 <div class="dag-loading"><div class="dag-spinner"></div><span>Building thought map...</span></div>216 </div>217 </div>218</div>219 220<footer>221 <div class="footer-inner">222 <span class="footer-brand">TunedAI Labs</span>223 <span>·</span>224 <span>Fine-tuned models for domains that matter</span>225 <span>·</span>226 <span>tunedailabs.com</span>227 </div>228</footer>229 230<script>231const SUGGESTED = """ + json.dumps(SUGGESTED) + """;232 233mermaid.initialize({startOnLoad:false,theme:'base',securityLevel:'loose',234 flowchart:{curve:'basis',htmlLabels:false,padding:20},235 themeVariables:{primaryColor:'#1c202e',primaryTextColor:'#e8eaf0',236 primaryBorderColor:'#c9a84c',lineColor:'#4a5568',237 secondaryColor:'#13161f',tertiaryColor:'#0a0c14'}});238 239const sugsEl = document.getElementById('sugs');240SUGGESTED.forEach(s => {241 const b = document.createElement('button');242 b.className = 'sug';243 b.textContent = s;244 b.onclick = () => { document.getElementById('q').value = s; ask(); };245 sugsEl.appendChild(b);246});247 248let rendered = false;249 250async function ask() {251 const q = document.getElementById('q').value.trim();252 if (!q) return;253 const btn = document.getElementById('askBtn');254 const out = document.getElementById('output');255 btn.disabled = true;256 btn.textContent = 'Thinking...';257 out.className = 'output show';258 out.innerHTML = '<span class="thinking">Entering the seminar...</span><span class="cursor"></span>';259 260 const warmTimer = setTimeout(() => {261 if (out.innerHTML.includes('Entering')) {262 out.innerHTML = '<span class="thinking">Model warming up — first response takes ~60s...</span><span class="cursor"></span>';263 }264 }, 8000);265 266 fetchDag(q);267 268 try {269 const res = await fetch('/stream', {270 method:'POST',271 headers:{'Content-Type':'application/json'},272 body: JSON.stringify({question: q, max_tokens: 2000})273 });274 const reader = res.body.getReader();275 const decoder = new TextDecoder();276 let text = '';277 out.innerHTML = '';278 clearTimeout(warmTimer);279 280 while (true) {281 const {done, value} = await reader.read();282 if (done) break;283 const lines = decoder.decode(value).split('\\n');284 for (const line of lines) {285 if (line.startsWith('data: ') && line !== 'data: [DONE]') {286 try {287 const d = JSON.parse(line.slice(6));288 if (d.token) {289 text += d.token;290 out.innerHTML = marked(text);291 }292 } catch(e) {}293 }294 }295 }296 } catch(e) {297 clearTimeout(warmTimer);298 out.textContent = 'Error: ' + e.message;299 }300 301 btn.disabled = false;302 btn.textContent = 'Ask';303}304 305// Simple markdown renderer306function marked(text) {307 return text308 .replace(/^### (.+)$/gm, '<h3>$1</h3>')309 .replace(/^## (.+)$/gm, '<h2>$1</h2>')310 .replace(/^# (.+)$/gm, '<h1>$1</h1>')311 .replace(/\\*\\*(.+?)\\*\\*/g, '<strong>$1</strong>')312 .replace(/\\*(.+?)\\*/g, '<em>$1</em>')313 .replace(/^---$/gm, '<hr>')314 .replace(/^> (.+)$/gm, '<blockquote>$1</blockquote>')315 .replace(/^- (.+)$/gm, '<li>$1</li>')316 .replace(/(<li>.*<\\/li>)/gs, '<ul>$1</ul>')317 .replace(/\\n\\n/g, '</p><p>')318 .replace(/^(?!<[h1-6ul]|<hr|<block)(.+)$/gm, '<p>$1</p>')319 .replace(/<p><\\/p>/g, '');320}321 322function sanitizeId(id) { return id.replace(/[^a-zA-Z0-9_]/g,'_'); }323function escapeLabel(l) { return l.replace(/"/g,'').replace(/'/g,'').replace(/[<>{}|]/g,''); }324 325async function fetchDag(question) {326 const wrap = document.getElementById('dagWrap');327 const body = document.getElementById('dagBody');328 const title = document.getElementById('dagTitle');329 wrap.className = 'dag-wrap show';330 body.innerHTML = '<div class="dag-loading"><div class="dag-spinner"></div><span>Mapping the philosophy...</span></div>';331 332 try {333 const res = await fetch('/dag', {334 method:'POST',335 headers:{'Content-Type':'application/json'},336 body: JSON.stringify({question})337 });338 const dag = await res.json();339 if (dag.error) { wrap.className = 'dag-wrap'; return; }340 title.textContent = dag.title || 'Thought Map';341 await renderDag(dag, body);342 } catch(e) {343 wrap.className = 'dag-wrap';344 }345}346 347async function renderDag(dag, container) {348 const lines = ['flowchart TD'];349 lines.push(' classDef root fill:#2a1c00,stroke:#c9a84c,stroke-width:3px,color:#e8c96a,font-weight:bold');350 lines.push(' classDef branch fill:#1a1d27,stroke:#c9a84c,stroke-width:2px,color:#e8c96a');351 lines.push(' classDef theory fill:#13161f,stroke:#4a7fb5,stroke-width:1px,color:#9da3b4');352 lines.push(' classDef philosopher fill:#0a0c14,stroke:#c9a84c,stroke-width:1px,color:#c9a84c');353 354 dag.nodes.forEach(n => {355 const sid = sanitizeId(n.id);356 const lbl = escapeLabel(n.label);357 if (n.type === 'root') lines.push(' ' + sid + '{"' + lbl + '"}');358 else if (n.type === 'branch') lines.push(' ' + sid + '["' + lbl + '"]');359 else if (n.type === 'theory') lines.push(' ' + sid + '("' + lbl + '")');360 else lines.push(' ' + sid + '(["' + lbl + '"])');361 lines.push(' class ' + sid + ' ' + n.type);362 });363 dag.edges.forEach(e => {364 lines.push(' ' + sanitizeId(e.from) + ' --> ' + sanitizeId(e.to));365 });366 367 const id = 'dag_' + Date.now();368 container.innerHTML = '<div class="mermaid" id="' + id + '">' + lines.join('\\n') + '</div>';369 try {370 await mermaid.run({nodes:[document.getElementById(id)]});371 } catch(e) {372 container.innerHTML = '<span style="color:var(--muted);font-size:12px">Map unavailable</span>';373 }374}375</script>376</body>377</html>"""378 379 380# ── ROUTES ────────────────────────────────────────────────────────────────────381 382@app.get("/", response_class=HTMLResponse)383async def root():384 return HTMLResponse(content=HTML, headers={"Cache-Control": "no-store, no-cache, must-revalidate"})385 386 387async def async_stream(url: str, model: str, system: str, question: str, max_tokens: int, auth_token: str):388 payload = {389 "model": model,390 "messages": [391 {"role": "system", "content": system},392 {"role": "user", "content": question}393 ],394 "max_tokens": max_tokens,395 "temperature": 0.7,396 "stream": True,397 }398 try:399 async with httpx.AsyncClient(timeout=600.0) as http:400 async with http.stream(401 "POST", f"{url}/chat/completions",402 json=payload,403 headers={"Authorization": f"Bearer {auth_token}", "Content-Type": "application/json"}404 ) as resp:405 async for line in resp.aiter_lines():406 if line.startswith("data: "):407 data = line[6:].strip()408 if data == "[DONE]":409 break410 try:411 chunk = json.loads(data)412 content = chunk["choices"][0]["delta"].get("content", "")413 if content:414 yield f"data: {json.dumps({'token': content})}\n\n"415 except Exception:416 pass417 except Exception as e:418 print(f"stream error: {e}", flush=True)419 yield "data: [DONE]\n\n"420 421 422@app.post("/stream")423async def stream(request: Request):424 body = await request.json()425 question = body.get("question", "")426 max_tokens = int(body.get("max_tokens", 2000))427 428 if PHILOSOPHER_MODEL_URL:429 return StreamingResponse(430 async_stream(PHILOSOPHER_MODEL_URL, "tgi", SYSTEM, question, max_tokens, HF_TOKEN),431 media_type="text/event-stream"432 )433 # Fallback to OpenAI434 async def openai_fallback():435 stream = client.chat.completions.create(436 model="gpt-4o",437 messages=[{"role": "system", "content": SYSTEM}, {"role": "user", "content": question}],438 stream=True, max_tokens=max_tokens,439 )440 for chunk in stream:441 if chunk.choices[0].delta.content:442 yield f"data: {json.dumps({'token': chunk.choices[0].delta.content})}\n\n"443 yield "data: [DONE]\n\n"444 return StreamingResponse(openai_fallback(), media_type="text/event-stream")445 446 447@app.post("/dag")448async def get_dag(request: Request):449 import asyncio450 body = await request.json()451 question = body.get("question", "")452 453 def _call():454 return client.chat.completions.create(455 model="gpt-4o-mini",456 messages=[457 {"role": "system", "content": DAG_SYSTEM},458 {"role": "user", "content": question}459 ],460 max_tokens=1200,461 temperature=0.3,462 response_format={"type": "json_object"},463 )464 465 try:466 response = await asyncio.get_running_loop().run_in_executor(None, _call)467 raw = response.choices[0].message.content468 text = raw.strip()469 if "```" in text:470 for part in text.split("```"):471 if part.startswith("json"): part = part[4:]472 part = part.strip()473 if part.startswith("{"):474 return JSONResponse(content=json.loads(part))475 start, end = text.find("{"), text.rfind("}") + 1476 if start >= 0 and end > start:477 return JSONResponse(content=json.loads(text[start:end]))478 return JSONResponse(content=json.loads(text))479 except Exception as e:480 return JSONResponse(content={"error": str(e)}, status_code=500)481 