Spaces:
Runtime error
Runtime error
Mark Gentry commited on
Commit ·
c510834
0
Parent(s):
Initial deploy — philosopher public site
Browse files- Dockerfile +7 -0
- README.md +13 -0
- philosopher_public.py +480 -0
- requirements.txt +5 -0
Dockerfile
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM python:3.11-slim
|
| 2 |
+
WORKDIR /app
|
| 3 |
+
COPY requirements.txt .
|
| 4 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
| 5 |
+
COPY philosopher_public.py .
|
| 6 |
+
EXPOSE 7860
|
| 7 |
+
CMD ["uvicorn", "philosopher_public:app", "--host", "0.0.0.0", "--port", "7860"]
|
README.md
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
title: Philosopher
|
| 3 |
+
emoji: 🏛️
|
| 4 |
+
colorFrom: yellow
|
| 5 |
+
colorTo: purple
|
| 6 |
+
sdk: docker
|
| 7 |
+
pinned: true
|
| 8 |
+
app_port: 7860
|
| 9 |
+
---
|
| 10 |
+
|
| 11 |
+
A fine-tuned philosophy professor. Deeper than GPT-4 on every question.
|
| 12 |
+
|
| 13 |
+
Built by [TunedAI Labs](https://tunedailabs.com).
|
philosopher_public.py
ADDED
|
@@ -0,0 +1,480 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Philosopher — Public Product Site
|
| 3 |
+
TunedAI Labs fine-tuned Qwen3.6-27B philosopher model.
|
| 4 |
+
Single panel, no password, focused on education/tutoring niche.
|
| 5 |
+
|
| 6 |
+
Run: uvicorn philosopher_public:app --port 8081
|
| 7 |
+
"""
|
| 8 |
+
|
| 9 |
+
import os
|
| 10 |
+
import json
|
| 11 |
+
import httpx
|
| 12 |
+
from fastapi import FastAPI, Request
|
| 13 |
+
from fastapi.responses import HTMLResponse, StreamingResponse, JSONResponse
|
| 14 |
+
from fastapi.middleware.cors import CORSMiddleware
|
| 15 |
+
from openai import OpenAI
|
| 16 |
+
|
| 17 |
+
app = FastAPI()
|
| 18 |
+
app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_methods=["*"], allow_headers=["*"])
|
| 19 |
+
|
| 20 |
+
PHILOSOPHER_MODEL_URL = os.environ.get("PHILOSOPHER_MODEL_URL", "")
|
| 21 |
+
HF_TOKEN = os.environ.get("HF_TOKEN", "not-needed")
|
| 22 |
+
OPENAI_API_KEY = os.environ.get("OPENAI_API_KEY", "")
|
| 23 |
+
|
| 24 |
+
client = OpenAI(api_key=OPENAI_API_KEY, timeout=20.0)
|
| 25 |
+
|
| 26 |
+
SYSTEM = 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 |
+
|
| 35 |
+
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.
|
| 36 |
+
|
| 37 |
+
Return 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 |
+
}
|
| 52 |
+
Rules:
|
| 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 maximum
|
| 58 |
+
- Philosopher labels: use the thinker's full common name
|
| 59 |
+
- Include at least 15 nodes total"""
|
| 60 |
+
|
| 61 |
+
SUGGESTED = [
|
| 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 |
+
|
| 74 |
+
HTML = """<!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 |
+
}
|
| 97 |
+
body{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}
|
| 120 |
+
textarea{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}
|
| 123 |
+
textarea:focus{border-color:var(--gold)}
|
| 124 |
+
textarea::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 */
|
| 172 |
+
footer{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>
|
| 231 |
+
const SUGGESTED = """ + json.dumps(SUGGESTED) + """;
|
| 232 |
+
|
| 233 |
+
mermaid.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 |
+
|
| 239 |
+
const sugsEl = document.getElementById('sugs');
|
| 240 |
+
SUGGESTED.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 |
+
|
| 248 |
+
let rendered = false;
|
| 249 |
+
|
| 250 |
+
async 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 renderer
|
| 306 |
+
function marked(text) {
|
| 307 |
+
return text
|
| 308 |
+
.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 |
+
|
| 322 |
+
function sanitizeId(id) { return id.replace(/[^a-zA-Z0-9_]/g,'_'); }
|
| 323 |
+
function escapeLabel(l) { return l.replace(/"/g,'').replace(/'/g,'').replace(/[<>{}|]/g,''); }
|
| 324 |
+
|
| 325 |
+
async 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 |
+
|
| 347 |
+
async 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)
|
| 383 |
+
async def root():
|
| 384 |
+
return HTMLResponse(content=HTML, headers={"Cache-Control": "no-store, no-cache, must-revalidate"})
|
| 385 |
+
|
| 386 |
+
|
| 387 |
+
async 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 |
+
break
|
| 410 |
+
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 |
+
pass
|
| 417 |
+
except Exception as e:
|
| 418 |
+
print(f"stream error: {e}", flush=True)
|
| 419 |
+
yield "data: [DONE]\n\n"
|
| 420 |
+
|
| 421 |
+
|
| 422 |
+
@app.post("/stream")
|
| 423 |
+
async 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 OpenAI
|
| 434 |
+
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")
|
| 448 |
+
async def get_dag(request: Request):
|
| 449 |
+
import asyncio
|
| 450 |
+
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.content
|
| 468 |
+
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("}") + 1
|
| 476 |
+
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)
|
requirements.txt
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
fastapi==0.115.5
|
| 2 |
+
uvicorn[standard]==0.32.1
|
| 3 |
+
openai==1.57.0
|
| 4 |
+
python-dotenv==1.0.1
|
| 5 |
+
httpx==0.27.2
|