Aobangaming commited on
Commit
4945f1a
·
verified ·
1 Parent(s): abec6b9

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +160 -26
app.py CHANGED
@@ -1,12 +1,13 @@
1
-
2
  import os
3
  import sys
 
4
  import uuid
 
5
  import threading
6
  import importlib.util
7
 
8
  import torch
9
- from flask import Flask, request, jsonify
10
  from flask_cors import CORS
11
  from transformers import AutoConfig, AutoModelForCausalLM
12
  from tokenizers import Tokenizer
@@ -25,6 +26,13 @@ TOP_P = 0.6
25
  PENALTY = 1.2
26
  TEMPERATURE = 1.0
27
 
 
 
 
 
 
 
 
28
  app = Flask(__name__)
29
  CORS(app)
30
 
@@ -43,8 +51,6 @@ print(f"[Bridge] Loading {MODEL_ID}...")
43
  device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
44
  print(f"[Bridge] Selected device: {device}")
45
 
46
- # Download and import Luna's actual custom architecture.
47
- # No separate Python file is required in your application folder.
48
  architecture_path = hf_hub_download(
49
  repo_id=MODEL_ID,
50
  filename="modeling_luna.py"
@@ -62,7 +68,6 @@ module = importlib.util.module_from_spec(spec)
62
  sys.modules[spec.name] = module
63
  spec.loader.exec_module(module)
64
 
65
- # Load Luna's custom Transformers configuration and model.
66
  config = AutoConfig.from_pretrained(
67
  MODEL_ID,
68
  trust_remote_code=True
@@ -77,7 +82,6 @@ model = AutoModelForCausalLM.from_pretrained(
77
  model = model.to(device)
78
  model.eval()
79
 
80
- # Load Luna's tokenizer.
81
  tokenizer_path = hf_hub_download(
82
  repo_id=MODEL_ID,
83
  filename="luna_tokenizer.json"
@@ -120,7 +124,154 @@ def generate_response(prompt, history):
120
 
121
 
122
  # ============================================================
123
- # Chat endpoint
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
124
  # ============================================================
125
 
126
  @app.route("/ask", methods=["POST"])
@@ -150,11 +301,8 @@ def ask():
150
  "logs": "Empty input."
151
  }), 400
152
 
153
- # The browser should reuse this ID for the same conversation.
154
  session_id = str(data.get("session_id") or uuid.uuid4())
155
 
156
- # Keep history retrieval, inference, and updates in one
157
- # critical section so concurrent requests cannot corrupt it.
158
  with generation_lock:
159
  history = sessions.setdefault(session_id, [])
160
 
@@ -183,7 +331,7 @@ def ask():
183
 
184
 
185
  # ============================================================
186
- # Clear conversation history
187
  # ============================================================
188
 
189
  @app.route("/clear", methods=["POST"])
@@ -205,20 +353,6 @@ def clear_history():
205
  })
206
 
207
 
208
- # ============================================================
209
- # Health check
210
- # ============================================================
211
-
212
- @app.route("/", methods=["GET"])
213
- def health():
214
- return jsonify({
215
- "service": "Aoban Luna 2 Flash",
216
- "model": MODEL_ID,
217
- "device": str(device),
218
- "status": "online"
219
- })
220
-
221
-
222
  # ============================================================
223
  # Start server
224
  # ============================================================
@@ -232,4 +366,4 @@ if __name__ == "__main__":
232
  host="0.0.0.0",
233
  port=port,
234
  threaded=True
235
- )
 
 
1
  import os
2
  import sys
3
+ import json
4
  import uuid
5
+ import hmac
6
  import threading
7
  import importlib.util
8
 
9
  import torch
10
+ from flask import Flask, request, jsonify, Response, stream_with_context
11
  from flask_cors import CORS
12
  from transformers import AutoConfig, AutoModelForCausalLM
13
  from tokenizers import Tokenizer
 
26
  PENALTY = 1.2
27
  TEMPERATURE = 1.0
28
 
29
+ # Poe sends this as "Authorization: Bearer <key>" on every request.
30
+ # Set it as a Secret in your Hugging Face Space (POE_ACCESS_KEY).
31
+ # If unset, Poe requests are accepted without auth (fine for testing only).
32
+ POE_ACCESS_KEY = os.environ.get("POE_ACCESS_KEY", "")
33
+
34
+ POE_INTRO_MESSAGE = "Hi! I'm Aoban Luna 2 Flash. Say something."
35
+
36
  app = Flask(__name__)
37
  CORS(app)
38
 
 
51
  device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
52
  print(f"[Bridge] Selected device: {device}")
53
 
 
 
54
  architecture_path = hf_hub_download(
55
  repo_id=MODEL_ID,
56
  filename="modeling_luna.py"
 
68
  sys.modules[spec.name] = module
69
  spec.loader.exec_module(module)
70
 
 
71
  config = AutoConfig.from_pretrained(
72
  MODEL_ID,
73
  trust_remote_code=True
 
82
  model = model.to(device)
83
  model.eval()
84
 
 
85
  tokenizer_path = hf_hub_download(
86
  repo_id=MODEL_ID,
87
  filename="luna_tokenizer.json"
 
124
 
125
 
126
  # ============================================================
127
+ # Poe helpers
128
+ # ============================================================
129
+
130
+ def sse(event, data):
131
+ """Format one Server-Sent Event the way Poe expects."""
132
+ return f"event: {event}\ndata: {json.dumps(data)}\n\n"
133
+
134
+
135
+ def poe_authorized():
136
+ if not POE_ACCESS_KEY:
137
+ return True
138
+
139
+ header = request.headers.get("Authorization", "")
140
+ token = header[7:] if header.lower().startswith("bearer ") else ""
141
+ return hmac.compare_digest(token, POE_ACCESS_KEY)
142
+
143
+
144
+ def poe_to_luna_history(query):
145
+ """
146
+ Poe sends the FULL conversation on every request, so Poe bots are
147
+ stateless. Convert it to (prompt, history) in Luna's format.
148
+
149
+ Poe roles: "system" | "user" | "bot"
150
+ Luna roles: "user" | "assistant"
151
+ """
152
+ messages = []
153
+
154
+ for msg in query:
155
+ if not isinstance(msg, dict):
156
+ continue
157
+
158
+ role = msg.get("role")
159
+ content = msg.get("content")
160
+
161
+ if not isinstance(content, str) or not content.strip():
162
+ continue
163
+
164
+ if role == "user":
165
+ messages.append({"role": "user", "content": content.strip()})
166
+ elif role == "bot":
167
+ messages.append({"role": "assistant", "content": content.strip()})
168
+ # "system" messages are ignored; Luna has no system prompt.
169
+
170
+ # The prompt is the latest user message; everything before it is history.
171
+ prompt = ""
172
+ history = []
173
+
174
+ for i in range(len(messages) - 1, -1, -1):
175
+ if messages[i]["role"] == "user":
176
+ prompt = messages[i]["content"]
177
+ history = messages[:i]
178
+ break
179
+
180
+ return prompt, history
181
+
182
+
183
+ def handle_poe_query(body):
184
+ prompt, history = poe_to_luna_history(body.get("query", []))
185
+
186
+ def stream():
187
+ yield sse("meta", {
188
+ "content_type": "text/markdown",
189
+ "linkify": True,
190
+ "suggested_replies": False
191
+ })
192
+
193
+ if not prompt:
194
+ yield sse("error", {
195
+ "text": "I didn't get a message to respond to.",
196
+ "allow_retry": False
197
+ })
198
+ yield sse("done", {})
199
+ return
200
+
201
+ try:
202
+ with generation_lock:
203
+ response = generate_response(prompt, history)
204
+
205
+ if not response:
206
+ response = "..."
207
+
208
+ yield sse("text", {"text": response})
209
+
210
+ except Exception as e:
211
+ app.logger.exception("Luna generation failed (Poe)")
212
+ yield sse("error", {
213
+ "text": f"Brain Error: {e}",
214
+ "allow_retry": True
215
+ })
216
+
217
+ yield sse("done", {})
218
+
219
+ return Response(
220
+ stream_with_context(stream()),
221
+ mimetype="text/event-stream",
222
+ headers={
223
+ "Cache-Control": "no-cache",
224
+ "X-Accel-Buffering": "no"
225
+ }
226
+ )
227
+
228
+
229
+ def handle_poe_request(body):
230
+ if not poe_authorized():
231
+ return jsonify({"error": "Unauthorized."}), 401
232
+
233
+ request_type = body.get("type")
234
+
235
+ if request_type == "query":
236
+ return handle_poe_query(body)
237
+
238
+ if request_type == "settings":
239
+ return jsonify({
240
+ "server_bot_dependencies": {},
241
+ "allow_attachments": False,
242
+ "introduction_message": POE_INTRO_MESSAGE
243
+ })
244
+
245
+ # "report_feedback" and "report_error" just need a 200 JSON reply.
246
+ return jsonify({})
247
+
248
+
249
+ # ============================================================
250
+ # Poe endpoint + health check (same URL: "/")
251
+ # ============================================================
252
+ # Poe POSTs to the bot URL you register (the Space's root URL).
253
+ # The browser/health check keeps using GET on the same route.
254
+
255
+ @app.route("/", methods=["GET", "POST"])
256
+ def root():
257
+ if request.method == "POST":
258
+ body = request.get_json(silent=True)
259
+
260
+ if not isinstance(body, dict) or "type" not in body:
261
+ return jsonify({"error": "Invalid Poe request."}), 400
262
+
263
+ return handle_poe_request(body)
264
+
265
+ return jsonify({
266
+ "service": "Aoban Luna 2 Flash",
267
+ "model": MODEL_ID,
268
+ "device": str(device),
269
+ "status": "online"
270
+ })
271
+
272
+
273
+ # ============================================================
274
+ # Chat endpoint (website / your JS script) - unchanged
275
  # ============================================================
276
 
277
  @app.route("/ask", methods=["POST"])
 
301
  "logs": "Empty input."
302
  }), 400
303
 
 
304
  session_id = str(data.get("session_id") or uuid.uuid4())
305
 
 
 
306
  with generation_lock:
307
  history = sessions.setdefault(session_id, [])
308
 
 
331
 
332
 
333
  # ============================================================
334
+ # Clear conversation history - unchanged
335
  # ============================================================
336
 
337
  @app.route("/clear", methods=["POST"])
 
353
  })
354
 
355
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
356
  # ============================================================
357
  # Start server
358
  # ============================================================
 
366
  host="0.0.0.0",
367
  port=port,
368
  threaded=True
369
+ )