Mark Gentry commited on
Commit
c510834
·
0 Parent(s):

Initial deploy — philosopher public site

Browse files
Files changed (4) hide show
  1. Dockerfile +7 -0
  2. README.md +13 -0
  3. philosopher_public.py +480 -0
  4. 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