Spaces:
Runtime error
Runtime error
| """ | |
| Philosopher β Public Product Site | |
| TunedAI Labs fine-tuned Qwen3.6-27B philosopher model. | |
| Single panel, no password, focused on education/tutoring niche. | |
| Run: uvicorn philosopher_public:app --port 8081 | |
| """ | |
| import os | |
| import json | |
| import httpx | |
| from fastapi import FastAPI, Request | |
| from fastapi.responses import HTMLResponse, StreamingResponse, JSONResponse | |
| from fastapi.middleware.cors import CORSMiddleware | |
| from openai import OpenAI | |
| app = FastAPI() | |
| app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_methods=["*"], allow_headers=["*"]) | |
| PHILOSOPHER_MODEL_URL = os.environ.get("PHILOSOPHER_MODEL_URL", "") | |
| HF_TOKEN = os.environ.get("HF_TOKEN", "not-needed") | |
| OPENAI_API_KEY = os.environ.get("OPENAI_API_KEY", "") | |
| client = OpenAI(api_key=OPENAI_API_KEY, timeout=20.0) | |
| SYSTEM = os.environ.get( | |
| "PHILOSOPHER_SYSTEM", | |
| "You are the world's best philosophy professor β more complete and deeper than any standard model. " | |
| "Cover every major theory, thinker, date, and work relevant to the question. Then go deeper: why did " | |
| "each thinker argue this, where does it hold up, where does it break down, how do the positions clash " | |
| "at the root level? End by showing the student the real disagreement underneath all positions and what " | |
| "remains genuinely open. Write in engaging prose. Be thorough but not padded." | |
| ) | |
| DAG_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. | |
| Return JSON with exactly this structure: | |
| { | |
| "title": "2-4 word topic label", | |
| "nodes": [ | |
| {"id": "ROOTID", "label": "display text (short)", "type": "root"}, | |
| {"id": "B1", "label": "Major Position Name", "type": "branch"}, | |
| {"id": "T1", "label": "Specific Theory", "type": "theory"}, | |
| {"id": "P1", "label": "Philosopher Name", "type": "philosopher"} | |
| ], | |
| "edges": [ | |
| {"from": "ROOTID", "to": "B1"}, | |
| {"from": "B1", "to": "T1"}, | |
| {"from": "T1", "to": "P1"} | |
| ] | |
| } | |
| Rules: | |
| - One root node: the central question or concept (type: "root") | |
| - 3 to 5 branch nodes: major philosophical camps or positions (type: "branch") | |
| - 2 to 3 theory nodes per branch: specific doctrines or arguments (type: "theory") | |
| - 1 to 3 philosopher nodes per theory or branch: individual thinkers (type: "philosopher") | |
| - Keep branch and theory labels SHORT: 2 to 4 words maximum | |
| - Philosopher labels: use the thinker's full common name | |
| - Include at least 15 nodes total""" | |
| SUGGESTED = [ | |
| "Is AI conscious?", | |
| "Does free will exist?", | |
| "What makes a life meaningful?", | |
| "Is morality objective or invented?", | |
| "Should I prioritize my happiness or my duty?", | |
| "What did Nietzsche actually believe?", | |
| "How do we know anything is real?", | |
| "Can science answer ethical questions?", | |
| "What is the self?", | |
| "Was Socrates right that wisdom begins with knowing you know nothing?", | |
| ] | |
| HTML = """<!DOCTYPE html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Philosopher β TunedAI Labs</title> | |
| <meta name="description" content="A philosophy professor in your pocket. Fine-tuned to teach, argue, and go deeper than any general AI."> | |
| <script src="https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.min.js"></script> | |
| <style> | |
| *{margin:0;padding:0;box-sizing:border-box} | |
| :root{ | |
| --bg:#0a0c14; | |
| --mid:#13161f; | |
| --light:#1c202e; | |
| --gold:#c9a84c; | |
| --gold-lite:#e8c96a; | |
| --purple:#7c6ef5; | |
| --purple-lite:#a99ff7; | |
| --text:#e8eaf0; | |
| --soft:#9da3b4; | |
| --muted:#6b7280; | |
| --border:#252836; | |
| } | |
| body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif; | |
| background:var(--bg);color:var(--text);min-height:100vh;display:flex;flex-direction:column} | |
| /* HERO */ | |
| .hero{padding:60px 24px 40px;text-align:center;border-bottom:1px solid var(--border)} | |
| .hero-badge{display:inline-block;background:rgba(201,168,76,.12);border:1px solid rgba(201,168,76,.3); | |
| color:var(--gold);font-size:11px;font-weight:700;letter-spacing:1.5px;text-transform:uppercase; | |
| padding:5px 14px;border-radius:20px;margin-bottom:20px} | |
| .hero h1{font-size:clamp(32px,6vw,56px);font-weight:900;letter-spacing:-1.5px; | |
| background:linear-gradient(135deg,var(--gold-lite),var(--gold),var(--purple-lite)); | |
| -webkit-background-clip:text;-webkit-text-fill-color:transparent;line-height:1.1;margin-bottom:16px} | |
| .hero p{font-size:clamp(15px,2vw,18px);color:var(--soft);max-width:560px;margin:0 auto 32px;line-height:1.6} | |
| .hero-meta{display:flex;justify-content:center;gap:24px;flex-wrap:wrap} | |
| .hero-meta span{font-size:12px;color:var(--muted);display:flex;align-items:center;gap:6px} | |
| .hero-meta span::before{content:'';display:inline-block;width:6px;height:6px;border-radius:50%;background:var(--gold);opacity:.7} | |
| /* MAIN LAYOUT */ | |
| .main{max-width:820px;margin:0 auto;width:100%;padding:32px 24px;flex:1} | |
| /* INPUT */ | |
| .input-wrap{background:var(--mid);border:1px solid var(--border);border-radius:16px; | |
| padding:20px;margin-bottom:24px} | |
| .input-row{display:flex;gap:12px;align-items:flex-end} | |
| textarea{flex:1;background:var(--bg);border:1px solid var(--border);color:var(--text); | |
| padding:14px 16px;border-radius:10px;font-size:15px;line-height:1.5;resize:none; | |
| min-height:56px;max-height:160px;outline:none;font-family:inherit} | |
| textarea:focus{border-color:var(--gold)} | |
| textarea::placeholder{color:var(--muted)} | |
| .ask-btn{background:linear-gradient(135deg,var(--gold),#a0782a);color:#0a0c14;border:none; | |
| padding:14px 28px;border-radius:10px;font-size:15px;font-weight:800;cursor:pointer; | |
| white-space:nowrap;transition:opacity .15s} | |
| .ask-btn:hover{opacity:.85} | |
| .ask-btn:disabled{opacity:.4;cursor:not-allowed} | |
| /* SUGGESTIONS */ | |
| .sugs{display:flex;flex-wrap:wrap;gap:8px;margin-top:14px} | |
| .sug{background:transparent;border:1px solid var(--border);color:var(--soft); | |
| font-size:12px;padding:6px 12px;border-radius:20px;cursor:pointer;transition:all .15s} | |
| .sug:hover{border-color:var(--gold);color:var(--gold-lite)} | |
| /* OUTPUT */ | |
| .output{background:var(--mid);border:1px solid var(--border);border-radius:16px; | |
| padding:28px;min-height:120px;display:none;line-height:1.75;font-size:15px} | |
| .output.show{display:block} | |
| .output h1,.output h2,.output h3{color:var(--gold-lite);margin:20px 0 8px;font-size:16px;font-weight:700} | |
| .output h1{font-size:20px;margin-top:0} | |
| .output p{margin-bottom:12px;color:var(--text)} | |
| .output strong{color:var(--gold-lite)} | |
| .output em{color:var(--soft)} | |
| .output hr{border:none;border-top:1px solid var(--border);margin:20px 0} | |
| .output ul,.output ol{padding-left:20px;margin-bottom:12px} | |
| .output li{margin-bottom:6px;color:var(--soft)} | |
| .output blockquote{border-left:3px solid var(--gold);padding-left:16px;color:var(--soft);margin:16px 0} | |
| .thinking{color:var(--muted);font-style:italic} | |
| .cursor{display:inline-block;width:2px;height:1em;background:var(--gold); | |
| margin-left:2px;vertical-align:text-bottom;animation:blink .8s infinite} | |
| @keyframes blink{0%,100%{opacity:1}50%{opacity:0}} | |
| /* DAG */ | |
| .dag-wrap{background:var(--mid);border:1px solid var(--border);border-radius:16px; | |
| margin-top:20px;overflow:hidden;display:none} | |
| .dag-wrap.show{display:block} | |
| .dag-hdr{padding:16px 20px;border-bottom:1px solid var(--border); | |
| display:flex;align-items:center;gap:10px} | |
| .dag-tag{background:rgba(201,168,76,.15);color:var(--gold);font-size:10px; | |
| font-weight:700;letter-spacing:1px;padding:3px 10px;border-radius:4px;text-transform:uppercase} | |
| .dag-title{font-size:13px;font-weight:600;color:var(--soft)} | |
| .dag-body{padding:20px;overflow-x:auto;min-height:80px} | |
| .dag-loading{display:flex;align-items:center;gap:10px;color:var(--muted);font-size:13px} | |
| .dag-spinner{width:16px;height:16px;border:2px solid var(--border); | |
| border-top-color:var(--gold);border-radius:50%;animation:spin .8s linear infinite} | |
| @keyframes spin{to{transform:rotate(360deg)}} | |
| .mermaid svg{max-width:100%;height:auto} | |
| /* FOOTER */ | |
| footer{padding:32px 24px;text-align:center;border-top:1px solid var(--border)} | |
| .footer-inner{display:flex;justify-content:center;align-items:center;gap:8px;flex-wrap:wrap} | |
| .footer-inner span{color:var(--muted);font-size:12px} | |
| .footer-brand{color:var(--gold);font-size:12px;font-weight:700} | |
| @media(max-width:600px){ | |
| .hero{padding:40px 16px 28px} | |
| .main{padding:20px 16px} | |
| .ask-btn{padding:14px 18px;font-size:14px} | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="hero"> | |
| <div class="hero-badge">TunedAI Labs</div> | |
| <h1>Philosopher</h1> | |
| <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> | |
| <div class="hero-meta"> | |
| <span>Qwen3.6-27B fine-tuned</span> | |
| <span>Seminar-style reasoning</span> | |
| <span>Deeper than GPT-4</span> | |
| </div> | |
| </div> | |
| <div class="main"> | |
| <div class="input-wrap"> | |
| <div class="input-row"> | |
| <textarea id="q" placeholder="Ask a philosophical question..." rows="2" | |
| onkeydown="if(event.key==='Enter'&&!event.shiftKey){event.preventDefault();ask()}"></textarea> | |
| <button class="ask-btn" id="askBtn" onclick="ask()">Ask</button> | |
| </div> | |
| <div class="sugs" id="sugs"></div> | |
| </div> | |
| <div class="output" id="output"></div> | |
| <div class="dag-wrap" id="dagWrap"> | |
| <div class="dag-hdr"> | |
| <span class="dag-tag">Thought Map</span> | |
| <span class="dag-title" id="dagTitle">Mapping the philosophy...</span> | |
| </div> | |
| <div class="dag-body" id="dagBody"> | |
| <div class="dag-loading"><div class="dag-spinner"></div><span>Building thought map...</span></div> | |
| </div> | |
| </div> | |
| </div> | |
| <footer> | |
| <div class="footer-inner"> | |
| <span class="footer-brand">TunedAI Labs</span> | |
| <span>Β·</span> | |
| <span>Fine-tuned models for domains that matter</span> | |
| <span>Β·</span> | |
| <span>tunedailabs.com</span> | |
| </div> | |
| </footer> | |
| <script> | |
| const SUGGESTED = """ + json.dumps(SUGGESTED) + """; | |
| mermaid.initialize({startOnLoad:false,theme:'base',securityLevel:'loose', | |
| flowchart:{curve:'basis',htmlLabels:false,padding:20}, | |
| themeVariables:{primaryColor:'#1c202e',primaryTextColor:'#e8eaf0', | |
| primaryBorderColor:'#c9a84c',lineColor:'#4a5568', | |
| secondaryColor:'#13161f',tertiaryColor:'#0a0c14'}}); | |
| const sugsEl = document.getElementById('sugs'); | |
| SUGGESTED.forEach(s => { | |
| const b = document.createElement('button'); | |
| b.className = 'sug'; | |
| b.textContent = s; | |
| b.onclick = () => { document.getElementById('q').value = s; ask(); }; | |
| sugsEl.appendChild(b); | |
| }); | |
| let rendered = false; | |
| async function ask() { | |
| const q = document.getElementById('q').value.trim(); | |
| if (!q) return; | |
| const btn = document.getElementById('askBtn'); | |
| const out = document.getElementById('output'); | |
| btn.disabled = true; | |
| btn.textContent = 'Thinking...'; | |
| out.className = 'output show'; | |
| out.innerHTML = '<span class="thinking">Entering the seminar...</span><span class="cursor"></span>'; | |
| const warmTimer = setTimeout(() => { | |
| if (out.innerHTML.includes('Entering')) { | |
| out.innerHTML = '<span class="thinking">Model warming up β first response takes ~60s...</span><span class="cursor"></span>'; | |
| } | |
| }, 8000); | |
| fetchDag(q); | |
| try { | |
| const res = await fetch('/stream', { | |
| method:'POST', | |
| headers:{'Content-Type':'application/json'}, | |
| body: JSON.stringify({question: q, max_tokens: 2000}) | |
| }); | |
| const reader = res.body.getReader(); | |
| const decoder = new TextDecoder(); | |
| let text = ''; | |
| out.innerHTML = ''; | |
| clearTimeout(warmTimer); | |
| while (true) { | |
| const {done, value} = await reader.read(); | |
| if (done) break; | |
| const lines = decoder.decode(value).split('\\n'); | |
| for (const line of lines) { | |
| if (line.startsWith('data: ') && line !== 'data: [DONE]') { | |
| try { | |
| const d = JSON.parse(line.slice(6)); | |
| if (d.token) { | |
| text += d.token; | |
| out.innerHTML = marked(text); | |
| } | |
| } catch(e) {} | |
| } | |
| } | |
| } | |
| } catch(e) { | |
| clearTimeout(warmTimer); | |
| out.textContent = 'Error: ' + e.message; | |
| } | |
| btn.disabled = false; | |
| btn.textContent = 'Ask'; | |
| } | |
| // Simple markdown renderer | |
| function marked(text) { | |
| return text | |
| .replace(/^### (.+)$/gm, '<h3>$1</h3>') | |
| .replace(/^## (.+)$/gm, '<h2>$1</h2>') | |
| .replace(/^# (.+)$/gm, '<h1>$1</h1>') | |
| .replace(/\\*\\*(.+?)\\*\\*/g, '<strong>$1</strong>') | |
| .replace(/\\*(.+?)\\*/g, '<em>$1</em>') | |
| .replace(/^---$/gm, '<hr>') | |
| .replace(/^> (.+)$/gm, '<blockquote>$1</blockquote>') | |
| .replace(/^- (.+)$/gm, '<li>$1</li>') | |
| .replace(/(<li>.*<\\/li>)/gs, '<ul>$1</ul>') | |
| .replace(/\\n\\n/g, '</p><p>') | |
| .replace(/^(?!<[h1-6ul]|<hr|<block)(.+)$/gm, '<p>$1</p>') | |
| .replace(/<p><\\/p>/g, ''); | |
| } | |
| function sanitizeId(id) { return id.replace(/[^a-zA-Z0-9_]/g,'_'); } | |
| function escapeLabel(l) { return l.replace(/"/g,'').replace(/'/g,'').replace(/[<>{}|]/g,''); } | |
| async function fetchDag(question) { | |
| const wrap = document.getElementById('dagWrap'); | |
| const body = document.getElementById('dagBody'); | |
| const title = document.getElementById('dagTitle'); | |
| wrap.className = 'dag-wrap show'; | |
| body.innerHTML = '<div class="dag-loading"><div class="dag-spinner"></div><span>Mapping the philosophy...</span></div>'; | |
| try { | |
| const res = await fetch('/dag', { | |
| method:'POST', | |
| headers:{'Content-Type':'application/json'}, | |
| body: JSON.stringify({question}) | |
| }); | |
| const dag = await res.json(); | |
| if (dag.error) { wrap.className = 'dag-wrap'; return; } | |
| title.textContent = dag.title || 'Thought Map'; | |
| await renderDag(dag, body); | |
| } catch(e) { | |
| wrap.className = 'dag-wrap'; | |
| } | |
| } | |
| async function renderDag(dag, container) { | |
| const lines = ['flowchart TD']; | |
| lines.push(' classDef root fill:#2a1c00,stroke:#c9a84c,stroke-width:3px,color:#e8c96a,font-weight:bold'); | |
| lines.push(' classDef branch fill:#1a1d27,stroke:#c9a84c,stroke-width:2px,color:#e8c96a'); | |
| lines.push(' classDef theory fill:#13161f,stroke:#4a7fb5,stroke-width:1px,color:#9da3b4'); | |
| lines.push(' classDef philosopher fill:#0a0c14,stroke:#c9a84c,stroke-width:1px,color:#c9a84c'); | |
| dag.nodes.forEach(n => { | |
| const sid = sanitizeId(n.id); | |
| const lbl = escapeLabel(n.label); | |
| if (n.type === 'root') lines.push(' ' + sid + '{"' + lbl + '"}'); | |
| else if (n.type === 'branch') lines.push(' ' + sid + '["' + lbl + '"]'); | |
| else if (n.type === 'theory') lines.push(' ' + sid + '("' + lbl + '")'); | |
| else lines.push(' ' + sid + '(["' + lbl + '"])'); | |
| lines.push(' class ' + sid + ' ' + n.type); | |
| }); | |
| dag.edges.forEach(e => { | |
| lines.push(' ' + sanitizeId(e.from) + ' --> ' + sanitizeId(e.to)); | |
| }); | |
| const id = 'dag_' + Date.now(); | |
| container.innerHTML = '<div class="mermaid" id="' + id + '">' + lines.join('\\n') + '</div>'; | |
| try { | |
| await mermaid.run({nodes:[document.getElementById(id)]}); | |
| } catch(e) { | |
| container.innerHTML = '<span style="color:var(--muted);font-size:12px">Map unavailable</span>'; | |
| } | |
| } | |
| </script> | |
| </body> | |
| </html>""" | |
| # ββ ROUTES ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| async def root(): | |
| return HTMLResponse(content=HTML, headers={"Cache-Control": "no-store, no-cache, must-revalidate"}) | |
| async def async_stream(url: str, model: str, system: str, question: str, max_tokens: int, auth_token: str): | |
| payload = { | |
| "model": model, | |
| "messages": [ | |
| {"role": "system", "content": system}, | |
| {"role": "user", "content": question} | |
| ], | |
| "max_tokens": max_tokens, | |
| "temperature": 0.7, | |
| "stream": True, | |
| } | |
| try: | |
| async with httpx.AsyncClient(timeout=600.0) as http: | |
| async with http.stream( | |
| "POST", f"{url}/chat/completions", | |
| json=payload, | |
| headers={"Authorization": f"Bearer {auth_token}", "Content-Type": "application/json"} | |
| ) as resp: | |
| async for line in resp.aiter_lines(): | |
| if line.startswith("data: "): | |
| data = line[6:].strip() | |
| if data == "[DONE]": | |
| break | |
| try: | |
| chunk = json.loads(data) | |
| content = chunk["choices"][0]["delta"].get("content", "") | |
| if content: | |
| yield f"data: {json.dumps({'token': content})}\n\n" | |
| except Exception: | |
| pass | |
| except Exception as e: | |
| print(f"stream error: {e}", flush=True) | |
| yield "data: [DONE]\n\n" | |
| async def stream(request: Request): | |
| body = await request.json() | |
| question = body.get("question", "") | |
| max_tokens = int(body.get("max_tokens", 2000)) | |
| if PHILOSOPHER_MODEL_URL: | |
| return StreamingResponse( | |
| async_stream(PHILOSOPHER_MODEL_URL, "tgi", SYSTEM, question, max_tokens, HF_TOKEN), | |
| media_type="text/event-stream" | |
| ) | |
| # Fallback to OpenAI | |
| async def openai_fallback(): | |
| stream = client.chat.completions.create( | |
| model="gpt-4o", | |
| messages=[{"role": "system", "content": SYSTEM}, {"role": "user", "content": question}], | |
| stream=True, max_tokens=max_tokens, | |
| ) | |
| for chunk in stream: | |
| if chunk.choices[0].delta.content: | |
| yield f"data: {json.dumps({'token': chunk.choices[0].delta.content})}\n\n" | |
| yield "data: [DONE]\n\n" | |
| return StreamingResponse(openai_fallback(), media_type="text/event-stream") | |
| async def get_dag(request: Request): | |
| import asyncio | |
| body = await request.json() | |
| question = body.get("question", "") | |
| def _call(): | |
| return client.chat.completions.create( | |
| model="gpt-4o-mini", | |
| messages=[ | |
| {"role": "system", "content": DAG_SYSTEM}, | |
| {"role": "user", "content": question} | |
| ], | |
| max_tokens=1200, | |
| temperature=0.3, | |
| response_format={"type": "json_object"}, | |
| ) | |
| try: | |
| response = await asyncio.get_running_loop().run_in_executor(None, _call) | |
| raw = response.choices[0].message.content | |
| text = raw.strip() | |
| if "```" in text: | |
| for part in text.split("```"): | |
| if part.startswith("json"): part = part[4:] | |
| part = part.strip() | |
| if part.startswith("{"): | |
| return JSONResponse(content=json.loads(part)) | |
| start, end = text.find("{"), text.rfind("}") + 1 | |
| if start >= 0 and end > start: | |
| return JSONResponse(content=json.loads(text[start:end])) | |
| return JSONResponse(content=json.loads(text)) | |
| except Exception as e: | |
| return JSONResponse(content={"error": str(e)}, status_code=500) | |