File size: 19,069 Bytes
c510834
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
"""
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 ────────────────────────────────────────────────────────────────────

@app.get("/", response_class=HTMLResponse)
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"


@app.post("/stream")
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")


@app.post("/dag")
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)