Ephraimmm commited on
Commit
ac8aabc
·
verified ·
1 Parent(s): c68ea64

Update handler.py

Browse files
Files changed (1) hide show
  1. handler.py +137 -552
handler.py CHANGED
@@ -1,596 +1,181 @@
1
  #tope_version
2
  # handler.py
3
  # handler.py
4
- # from typing import Any, Dict, List, Union, Optional
5
- # import torch
6
- # from huggingface_inference_toolkit.logging import logger
7
- # from unsloth import FastLanguageModel
8
- # from peft import PeftModel
9
-
10
- # # OpenAI-style prompt:
11
- # # - str
12
- # # - [{"role": "...", "content": "..." | [{"type":"text","text":"..."}]}]
13
- # Prompt = Union[str, List[Dict[str, Any]], Dict[str, Any]]
14
-
15
-
16
- # def _to_block_list(content: Any) -> List[Dict[str, str]]:
17
- # """Ensure content is a list[{'type':'text','text':...}]."""
18
- # if content is None:
19
- # return [{"type": "text", "text": ""}]
20
- # if isinstance(content, list):
21
- # # Assume already blocks
22
- # return content
23
- # # Fallback: stringify
24
- # return [{"type": "text", "text": str(content)}]
25
-
26
-
27
- # class EndpointHandler:
28
- # """
29
- # HF Inference Endpoint handler that mirrors your Gradio setup:
30
- # - Loads Unsloth 4-bit Gemma-3-4B IT
31
- # - Attaches LoRA adapter
32
- # - Uses tokenizer.apply_chat_template
33
- # - Always replies in Nigerian Pidgin
34
- # """
35
-
36
- # def __init__(self, model_dir: str, **_: Any):
37
- # logger.info(f"Initializing with model_dir={model_dir}")
38
-
39
- # base_model_name = "unsloth/gemma-3-4b-it-unsloth-bnb-4bit"
40
- # model, tokenizer = FastLanguageModel.from_pretrained(
41
- # model_name=base_model_name,
42
- # max_seq_length=2048,
43
- # dtype=torch.float16,
44
- # load_in_4bit=True,
45
- # )
46
-
47
- # lora_repo = "Ephraimmm/PIDGIN_gemma-3"
48
- # model = PeftModel.from_pretrained(model, lora_repo)
49
- # FastLanguageModel.for_inference(model)
50
-
51
- # self.model = model.eval()
52
- # self.tokenizer = tokenizer
53
- # self.device = getattr(
54
- # self.model,
55
- # "device",
56
- # torch.device("cuda" if torch.cuda.is_available() else "cpu"),
57
- # )
58
- # # Safety: some tokenizers lack pad_token_id
59
- # if getattr(self.tokenizer, "pad_token_id", None) is None:
60
- # self.tokenizer.pad_token_id = self.tokenizer.eos_token_id
61
-
62
- # logger.info(f"Device: {self.device} | eos_id: {self.tokenizer.eos_token_id}")
63
-
64
- # self._system_text = (
65
- # "You are a Nigerian assistant that speaks PIDGIN ENGLISH. "
66
- # "When asked 'how far', reply 'I dey o, how you dey?'. "
67
- # "Always answer in Pidgin English."
68
- # )
69
-
70
- # def _normalize_messages(self, prompt: Prompt) -> List[Dict[str, Any]]:
71
- # """Return a list of messages with block-style contents only."""
72
- # # Accept single dict
73
- # if isinstance(prompt, dict):
74
- # prompt = [prompt]
75
-
76
- # # Accept raw string
77
- # if isinstance(prompt, str):
78
- # msgs = [
79
- # {"role": "system", "content": _to_block_list(self._system_text)},
80
- # {"role": "user", "content": _to_block_list(prompt)},
81
- # ]
82
- # return msgs
83
-
84
- # if not isinstance(prompt, list):
85
- # raise ValueError("`inputs` must be a string, a message dict, or a list of messages.")
86
-
87
- # # Normalize all contents to blocks
88
- # norm = []
89
- # for m in prompt:
90
- # role = m.get("role", "user")
91
- # content = _to_block_list(m.get("content", ""))
92
- # norm.append({"role": role, "content": content})
93
-
94
- # # Ensure a system message exists (prepend if missing)
95
- # if not any(m.get("role") == "system" for m in norm):
96
- # norm = [{"role": "system", "content": _to_block_list(self._system_text)}] + norm
97
-
98
- # return norm
99
-
100
- # def _build_model_inputs(self, prompt: Prompt):
101
- # messages = self._normalize_messages(prompt)
102
- # # Use the same path as your Gradio script
103
- # model_inputs = self.tokenizer.apply_chat_template(
104
- # messages,
105
- # add_generation_prompt=True,
106
- # return_tensors="pt",
107
- # tokenize=True,
108
- # return_dict=True,
109
- # ).to(self.device)
110
- # return model_inputs
111
-
112
- # def __call__(self, data: Dict[str, Any]) -> Dict[str, Any]:
113
- # logger.info(f"Incoming keys: {list(data.keys())}")
114
- # if "inputs" not in data:
115
- # raise ValueError("Missing `inputs` in request body.")
116
-
117
- # prompt: Prompt = data["inputs"]
118
- # params: Dict[str, Any] = (data.get("parameters") or {})
119
-
120
- # # Defaults aligned with your Gradio code
121
- # max_new_tokens = int(params.get("max_new_tokens", 256)) # raise per request if needed
122
- # temperature = float(params.get("temperature", 0.1))
123
- # top_p = float(params.get("top_p", 1.0))
124
- # top_k: Optional[int] = params.get("top_k", None)
125
- # use_cache = bool(params.get("use_cache", False))
126
- # repetition_penalty = float(params.get("repetition_penalty", 1.0))
127
- # eos_token_id = params.get("eos_token_id", self.tokenizer.eos_token_id)
128
- # pad_token_id = params.get("pad_token_id", self.tokenizer.pad_token_id)
129
-
130
- # inputs = self._build_model_inputs(prompt)
131
-
132
- # gen_kwargs = dict(
133
- # **inputs,
134
- # max_new_tokens=max_new_tokens,
135
- # temperature=temperature,
136
- # top_p=top_p,
137
- # do_sample=True, # streaming-like sampling on
138
- # use_cache=use_cache,
139
- # repetition_penalty=repetition_penalty,
140
- # eos_token_id=eos_token_id,
141
- # pad_token_id=pad_token_id,
142
- # )
143
- # if top_k is not None:
144
- # gen_kwargs["top_k"] = int(top_k)
145
-
146
- # with torch.no_grad():
147
- # output_ids = self.model.generate(**gen_kwargs)
148
-
149
- # # Return ONLY the continuation (like your streamer)
150
- # input_len = inputs["input_ids"].shape[-1]
151
- # generated_ids = output_ids[0, input_len:]
152
- # reply_text = self.tokenizer.decode(generated_ids, skip_special_tokens=True)
153
-
154
- # return {
155
- # "reply": [reply_text],
156
- # "usage": {
157
- # "prompt_tokens": int(input_len),
158
- # "generated_tokens": int(generated_ids.shape[-1]),
159
- # },
160
- # }
161
-
162
-
163
- # if __name__ == "__main__":
164
- # h = EndpointHandler(model_dir=".")
165
- # # Raw string
166
- # print(h({"inputs": "How far?", "parameters": {"max_new_tokens": 64}}))
167
- # # Block-style content (your Gradio shape)
168
- # print(h({
169
- # "inputs": [
170
- # {
171
- # "role": "user",
172
- # "content": [{"type": "text", "text": "Explain why sky dey blue."}]
173
- # }
174
- # ],
175
- # "parameters": {"max_new_tokens": 64}
176
- # }))
177
-
178
-
179
-
180
- #OPENAI TRIAL
181
- # handler.py
182
- import os
183
- import json
184
- import time
185
  import torch
186
- from typing import Any, Dict, List, Optional, Iterator
187
-
188
- # Note: these imports match your environment
189
  from huggingface_inference_toolkit.logging import logger
190
  from unsloth import FastLanguageModel
191
  from peft import PeftModel
192
-
193
- # Try to import transformers' streaming helper (best-effort)
194
- try:
195
- from transformers import TextIteratorStreamer
196
- _HAS_STREAMER = True
197
- except Exception:
198
- _HAS_STREAMER = False
199
-
200
-
201
- # ----- Config / constants -----
202
- BASE_MODEL = "unsloth/gemma-3-4b-it-unsloth-bnb-4bit"
203
- LORA_REPO = "Ephraimmm/PIDGIN_gemma-3"
204
-
205
- # Fixed system prompt (ALWAYS injected)
206
- PIDGIN_SYSTEM = (
207
- "You be AI assistant always answer strictly for Nigerian pidgin. "
208
- "Keep grammar natural like Naija street pidgin. "
209
- "No switch go Queen English."
210
- )
211
-
212
-
213
- # ----- Helper utilities -----
214
- def _to_text_blocks(content: Any) -> List[Dict[str, str]]:
215
- """Ensure content is a list of {'type':'text','text':...} blocks."""
216
  if content is None:
217
  return [{"type": "text", "text": ""}]
218
  if isinstance(content, list):
 
219
  return content
 
220
  return [{"type": "text", "text": str(content)}]
221
-
222
-
223
- def _normalize_openai_messages(messages: Any) -> List[Dict[str, Any]]:
224
- """
225
- Normalize OpenAI-style messages to a list of dicts with block-style content.
226
- Also ALWAYS prepend PIDGIN_SYSTEM as the first system message.
227
- """
228
- if isinstance(messages, str):
229
- msgs = [
230
- {"role": "system", "content": _to_text_blocks(PIDGIN_SYSTEM)},
231
- {"role": "user", "content": _to_text_blocks(messages)},
232
- ]
233
- return msgs
234
-
235
- if isinstance(messages, dict):
236
- messages = [messages]
237
-
238
- if not isinstance(messages, list):
239
- raise ValueError("`messages` must be a string, dict, or list of message dicts.")
240
-
241
- norm = []
242
- for m in messages:
243
- role = m.get("role", "user")
244
- content = m.get("content", "")
245
- # Accept both raw string content or block-style content
246
- if isinstance(content, str):
247
- content_blocks = _to_text_blocks(content)
248
- else:
249
- content_blocks = _to_text_blocks(content)
250
- norm.append({"role": role, "content": content_blocks})
251
-
252
- # ALWAYS prepend the pidgin system message
253
- norm = [{"role": "system", "content": _to_text_blocks(PIDGIN_SYSTEM)}] + norm
254
- return norm
255
-
256
-
257
- # ----- Endpoint handler -----
258
  class EndpointHandler:
259
  """
260
- HuggingFace Inference Endpoint handler.
261
-
262
- Expects OpenAI-style body:
263
- {
264
- "messages": [...],
265
- "max_tokens": 256,
266
- "temperature": 0.7,
267
- "top_p": 0.9,
268
- "stream": true|false,
269
- ...
270
- }
271
-
272
- When stream=True this returns a generator yielding JSONL string chunks
273
- in OpenAI-like streaming format (data: ...\n\n).
274
  """
275
- def __init__(self, model_dir: str = "", **kwargs: Any):
276
- logger.info(f"Initializing EndpointHandler, model_dir={model_dir}")
277
-
278
- # Load base model (unsloth 4-bit) and tokenizer via FastLanguageModel
279
- # returns (model, tokenizer)
280
  model, tokenizer = FastLanguageModel.from_pretrained(
281
- model_name=BASE_MODEL,
282
  max_seq_length=2048,
283
  dtype=torch.float16,
284
  load_in_4bit=True,
285
  )
286
-
287
- # Attach LoRA adapter (not merged)
288
- model = PeftModel.from_pretrained(model, LORA_REPO)
289
-
290
- # Prepare for inference (unsloth helper)
291
  FastLanguageModel.for_inference(model)
292
-
293
  self.model = model.eval()
294
  self.tokenizer = tokenizer
295
-
296
- # device (some wrappers set it on the model)
297
- self.device = getattr(self.model, "device", torch.device("cuda" if torch.cuda.is_available() else "cpu"))
298
-
299
- # safety: ensure pad token exists
 
300
  if getattr(self.tokenizer, "pad_token_id", None) is None:
301
  self.tokenizer.pad_token_id = self.tokenizer.eos_token_id
302
-
303
- logger.info(f"Loaded model. Device={self.device}, eos_id={self.tokenizer.eos_token_id}")
304
-
305
- # Build tokenized inputs using tokenizer.apply_chat_template if available
306
- def _build_inputs(self, messages: Any):
307
- norm_msgs = _normalize_openai_messages(messages)
308
- if hasattr(self.tokenizer, "apply_chat_template"):
309
- # Mirror your existing pipeline: add_generation_prompt=True, tokenize=True/return_tensors
310
- model_inputs = self.tokenizer.apply_chat_template(
311
- norm_msgs,
312
- add_generation_prompt=True,
313
- return_tensors="pt",
314
- tokenize=True,
315
- return_dict=True,
316
- ).to(self.device)
317
- else:
318
- # Fallback: naive formatting (Gemma chat format)
319
- formatted = []
320
- for m in norm_msgs:
321
- role = m.get("role", "user")
322
- text = "".join([b.get("text", "") for b in m.get("content", [])])
323
- if role in ("system", "user"):
324
- formatted.append(f"<start_of_turn>user\n{text}<end_of_turn>\n")
325
- else:
326
- formatted.append(f"<start_of_turn>model\n{text}<end_of_turn>\n")
327
- formatted.append("<start_of_turn>model\n")
328
- prompt = "".join(formatted)
329
- model_inputs = self.tokenizer(
330
- prompt,
331
- return_tensors="pt",
332
- truncation=True,
333
- max_length=2048
334
- ).to(self.device)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
335
  return model_inputs
336
-
337
- # Helper to produce OpenAI-style id
338
- def _make_id(self) -> str:
339
- return f"chatcmpl-{os.urandom(12).hex()}"
340
-
341
- # Streaming generator: best-effort using TextIteratorStreamer if available,
342
- # otherwise falling back to chunking final text.
343
- def _stream_generator(
344
- self,
345
- model_inputs: Dict[str, Any],
346
- gen_kwargs: Dict[str, Any],
347
- model_id: str,
348
- index: int = 0,
349
- chunk_size: int = 32,
350
- ) -> Iterator[bytes]:
351
- """
352
- Yields bytes (JSONL lines) compatible with OpenAI streaming:
353
- - data: <json>\n\n for each chunk
354
- - data: [DONE]\n\n at end
355
- Uses TextIteratorStreamer if available; otherwise generates full text and
356
- yields sliced chunks (best-effort).
357
- """
358
- # Prepare some metadata
359
- prompt_tokens = int(model_inputs["input_ids"].shape[1])
360
- generated_tokens_so_far = 0
361
- choices_metadata = {"index": index, "finish_reason": None}
362
-
363
- # Try TextIteratorStreamer route (token-by-token streaming)
364
- if _HAS_STREAMER:
365
- try:
366
- streamer = TextIteratorStreamer(self.tokenizer, skip_prompt=True, skip_special_tokens=True)
367
- gen_kwargs_stream = gen_kwargs.copy()
368
- gen_kwargs_stream["streamer"] = streamer
369
-
370
- # Kick off generation in a background thread to avoid blocking the generator;
371
- # but since we are in a single-process environment, we'll call generate in another thread.
372
- import threading
373
- def _generate():
374
- with torch.no_grad():
375
- # model.generate will populate streamer as tokens arrive
376
- self.model.generate(**gen_kwargs_stream)
377
-
378
- gen_thread = threading.Thread(target=_generate, daemon=True)
379
- gen_thread.start()
380
-
381
- # Iterate streamer for pieces of text
382
- buffer = ""
383
- for piece in streamer:
384
- if not piece:
385
- continue
386
- buffer += piece
387
- # when buffer reaches chunk_size, yield a chunk
388
- while len(buffer) >= chunk_size:
389
- chunk_text = buffer[:chunk_size]
390
- buffer = buffer[chunk_size:]
391
- generated_tokens_so_far += len(self.tokenizer.encode(chunk_text, add_special_tokens=False))
392
- data = {
393
- "id": model_id,
394
- "object": "chat.completion.chunk",
395
- "choices": [
396
- {
397
- "delta": {"content": chunk_text},
398
- "index": index,
399
- "finish_reason": None,
400
- }
401
- ],
402
- }
403
- yield (f"data: {json.dumps(data)}\n\n").encode("utf-8")
404
-
405
- # flush remaining buffer
406
- if buffer:
407
- generated_tokens_so_far += len(self.tokenizer.encode(buffer, add_special_tokens=False))
408
- data = {
409
- "id": model_id,
410
- "object": "chat.completion.chunk",
411
- "choices": [
412
- {
413
- "delta": {"content": buffer},
414
- "index": index,
415
- "finish_reason": None,
416
- }
417
- ],
418
- }
419
- yield (f"data: {json.dumps(data)}\n\n").encode("utf-8")
420
-
421
- # Wait for generation to finish
422
- gen_thread.join(timeout=1.0)
423
-
424
- # final "done" event with finish_reason and usage (best-effort)
425
- completion_tokens = generated_tokens_so_far
426
- total_tokens = prompt_tokens + completion_tokens
427
- final_data = {
428
- "id": model_id,
429
- "object": "chat.completion",
430
- "choices": [
431
- {
432
- "index": index,
433
- "message": {
434
- "role": "assistant",
435
- "content": "", # full content is not repeated in chunk streaming
436
- },
437
- "finish_reason": "stop",
438
- }
439
- ],
440
- "usage": {
441
- "prompt_tokens": prompt_tokens,
442
- "completion_tokens": completion_tokens,
443
- "total_tokens": total_tokens,
444
- },
445
- }
446
- # final chunk with DONE marker
447
- yield (f"data: {json.dumps(final_data)}\n\n").encode("utf-8")
448
- yield b"data: [DONE]\n\n"
449
- return
450
-
451
- except Exception as ex:
452
- logger.warning(f"TextIteratorStreamer failed, falling back to chunking. Reason: {ex}")
453
-
454
- # Fallback: non-streaming generate then chunk it
455
- with torch.no_grad():
456
- output_ids = self.model.generate(**gen_kwargs)
457
- input_len = model_inputs["input_ids"].shape[-1]
458
- generated_ids = output_ids[0, input_len:]
459
- full_text = self.tokenizer.decode(generated_ids, skip_special_tokens=True)
460
- # yield chunks
461
- start = 0
462
- while start < len(full_text):
463
- chunk_text = full_text[start:start + chunk_size]
464
- start += chunk_size
465
- data = {
466
- "id": model_id,
467
- "object": "chat.completion.chunk",
468
- "choices": [
469
- {
470
- "delta": {"content": chunk_text},
471
- "index": index,
472
- "finish_reason": None,
473
- }
474
- ],
475
- }
476
- yield (f"data: {json.dumps(data)}\n\n").encode("utf-8")
477
- time.sleep(0.01) # small pause to give a streaming feel
478
-
479
- # final metadata
480
- completion_tokens = int(generated_ids.shape[-1])
481
- total_tokens = prompt_tokens + completion_tokens
482
- final_data = {
483
- "id": model_id,
484
- "object": "chat.completion",
485
- "choices": [
486
- {
487
- "index": index,
488
- "message": {
489
- "role": "assistant",
490
- "content": full_text,
491
- },
492
- "finish_reason": "stop",
493
- }
494
- ],
495
- "usage": {
496
- "prompt_tokens": prompt_tokens,
497
- "completion_tokens": completion_tokens,
498
- "total_tokens": total_tokens,
499
- },
500
- }
501
- yield (f"data: {json.dumps(final_data)}\n\n").encode("utf-8")
502
- yield b"data: [DONE]\n\n"
503
-
504
- # The core entrypoint used by HF Inference Endpoint
505
- def __call__(self, data: Dict[str, Any]):
506
- """
507
- Expect OpenAI-style body. Returns either:
508
- - dict (full OpenAI response) if stream=False
509
- - generator yielding bytes (JSONL) if stream=True
510
- """
511
- # Validate
512
- messages = data.get("messages")
513
- if messages is None:
514
- raise ValueError("Missing required field `messages` (OpenAI chat format).")
515
-
516
- # read parameters (OpenAI names)
517
- max_tokens = int(data.get("max_tokens", 256))
518
- temperature = float(data.get("temperature", 0.7))
519
- top_p = float(data.get("top_p", 0.9))
520
- top_k = data.get("top_k", None)
521
- stream = bool(data.get("stream", False))
522
- do_sample = temperature > 0.0
523
-
524
- # Build model inputs (normalizes messages and ALWAYS injects system prompt)
525
- model_inputs = self._build_inputs(messages)
526
-
527
- # generation kwargs (map OpenAI names to HF generate)
528
  gen_kwargs = dict(
529
- **model_inputs,
530
- max_new_tokens=max_tokens,
531
  temperature=temperature,
532
  top_p=top_p,
533
- do_sample=do_sample,
534
- pad_token_id=self.tokenizer.pad_token_id or self.tokenizer.eos_token_id,
535
- eos_token_id=self.tokenizer.eos_token_id,
 
 
536
  )
537
  if top_k is not None:
538
  gen_kwargs["top_k"] = int(top_k)
539
-
540
- model_id = self._make_id()
541
-
542
- # STREAMING behavior: return generator of bytes (JSONL data:)
543
- if stream:
544
- return self._stream_generator(model_inputs=model_inputs, gen_kwargs=gen_kwargs, model_id=model_id)
545
-
546
- # NON-STREAMING: generate and return full OpenAI-style completion dict
547
  with torch.no_grad():
548
  output_ids = self.model.generate(**gen_kwargs)
549
-
550
- input_len = model_inputs["input_ids"].shape[-1]
 
551
  generated_ids = output_ids[0, input_len:]
552
- generated_text = self.tokenizer.decode(generated_ids, skip_special_tokens=True)
553
-
554
- prompt_tokens = int(input_len)
555
- completion_tokens = int(generated_ids.shape[-1])
556
- total_tokens = prompt_tokens + completion_tokens
557
-
558
- resp = {
559
- "id": model_id,
560
- "object": "chat.completion",
561
- "created": int(time.time()),
562
- "model": LORA_REPO,
563
- "choices": [
564
- {
565
- "index": 0,
566
- "message": {
567
- "role": "assistant",
568
- "content": generated_text.strip(),
569
- },
570
- "finish_reason": "stop",
571
- }
572
- ],
573
  "usage": {
574
- "prompt_tokens": prompt_tokens,
575
- "completion_tokens": completion_tokens,
576
- "total_tokens": total_tokens,
577
  },
578
  }
579
- return resp
580
-
581
-
582
- # If you want to smoke-test locally
583
  if __name__ == "__main__":
584
  h = EndpointHandler(model_dir=".")
585
- req = {
586
- "messages": [
587
- {"role": "user", "content": "How far?"},
 
 
 
 
 
 
588
  ],
589
- "max_tokens": 64,
590
- "temperature": 0.2,
591
- "stream": False,
592
- }
593
- print(json.dumps(h(req), indent=2)[:1000])
594
 
595
 
596
  # from typing import Any, Dict, List, Union, Optional
 
1
  #tope_version
2
  # handler.py
3
  # handler.py
4
+ from typing import Any, Dict, List, Union, Optional
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5
  import torch
 
 
 
6
  from huggingface_inference_toolkit.logging import logger
7
  from unsloth import FastLanguageModel
8
  from peft import PeftModel
9
+
10
+ # OpenAI-style prompt:
11
+ # - str
12
+ # - [{"role": "...", "content": "..." | [{"type":"text","text":"..."}]}]
13
+ Prompt = Union[str, List[Dict[str, Any]], Dict[str, Any]]
14
+
15
+
16
+ def _to_block_list(content: Any) -> List[Dict[str, str]]:
17
+ """Ensure content is a list[{'type':'text','text':...}]."""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
18
  if content is None:
19
  return [{"type": "text", "text": ""}]
20
  if isinstance(content, list):
21
+ # Assume already blocks
22
  return content
23
+ # Fallback: stringify
24
  return [{"type": "text", "text": str(content)}]
25
+
26
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
27
  class EndpointHandler:
28
  """
29
+ HF Inference Endpoint handler that mirrors your Gradio setup:
30
+ - Loads Unsloth 4-bit Gemma-3-4B IT
31
+ - Attaches LoRA adapter
32
+ - Uses tokenizer.apply_chat_template
33
+ - Always replies in Nigerian Pidgin
 
 
 
 
 
 
 
 
 
34
  """
35
+
36
+ def __init__(self, model_dir: str, **_: Any):
37
+ logger.info(f"Initializing with model_dir={model_dir}")
38
+
39
+ base_model_name = "unsloth/gemma-3-4b-it-unsloth-bnb-4bit"
40
  model, tokenizer = FastLanguageModel.from_pretrained(
41
+ model_name=base_model_name,
42
  max_seq_length=2048,
43
  dtype=torch.float16,
44
  load_in_4bit=True,
45
  )
46
+
47
+ lora_repo = "Ephraimmm/PIDGIN_gemma-3"
48
+ model = PeftModel.from_pretrained(model, lora_repo)
 
 
49
  FastLanguageModel.for_inference(model)
50
+
51
  self.model = model.eval()
52
  self.tokenizer = tokenizer
53
+ self.device = getattr(
54
+ self.model,
55
+ "device",
56
+ torch.device("cuda" if torch.cuda.is_available() else "cpu"),
57
+ )
58
+ # Safety: some tokenizers lack pad_token_id
59
  if getattr(self.tokenizer, "pad_token_id", None) is None:
60
  self.tokenizer.pad_token_id = self.tokenizer.eos_token_id
61
+
62
+ logger.info(f"Device: {self.device} | eos_id: {self.tokenizer.eos_token_id}")
63
+
64
+ self._system_text = (
65
+ "You are a Nigerian assistant that speaks PIDGIN ENGLISH. "
66
+ "When asked 'how far', reply 'I dey o, how you dey?'. "
67
+ "Always answer in Pidgin English."
68
+ )
69
+
70
+ def _normalize_messages(self, prompt: Prompt) -> List[Dict[str, Any]]:
71
+ """Return a list of messages with block-style contents only."""
72
+ # Accept single dict
73
+ if isinstance(prompt, dict):
74
+ prompt = [prompt]
75
+
76
+ # Accept raw string
77
+ if isinstance(prompt, str):
78
+ msgs = [
79
+ {"role": "system", "content": _to_block_list(self._system_text)},
80
+ {"role": "user", "content": _to_block_list(prompt)},
81
+ ]
82
+ return msgs
83
+
84
+ if not isinstance(prompt, list):
85
+ raise ValueError("`inputs` must be a string, a message dict, or a list of messages.")
86
+
87
+ # Normalize all contents to blocks
88
+ norm = []
89
+ for m in prompt:
90
+ role = m.get("role", "user")
91
+ content = _to_block_list(m.get("content", ""))
92
+ norm.append({"role": role, "content": content})
93
+
94
+ # Ensure a system message exists (prepend if missing)
95
+ if not any(m.get("role") == "system" for m in norm):
96
+ norm = [{"role": "system", "content": _to_block_list(self._system_text)}] + norm
97
+
98
+ return norm
99
+
100
+ def _build_model_inputs(self, prompt: Prompt):
101
+ messages = self._normalize_messages(prompt)
102
+ # Use the same path as your Gradio script
103
+ model_inputs = self.tokenizer.apply_chat_template(
104
+ messages,
105
+ add_generation_prompt=True,
106
+ return_tensors="pt",
107
+ tokenize=True,
108
+ return_dict=True,
109
+ ).to(self.device)
110
  return model_inputs
111
+
112
+ def __call__(self, data: Dict[str, Any]) -> Dict[str, Any]:
113
+ logger.info(f"Incoming keys: {list(data.keys())}")
114
+ if "inputs" not in data:
115
+ raise ValueError("Missing `inputs` in request body.")
116
+
117
+ prompt: Prompt = data["inputs"]
118
+ params: Dict[str, Any] = (data.get("parameters") or {})
119
+
120
+ # Defaults aligned with your Gradio code
121
+ max_new_tokens = int(params.get("max_new_tokens", 256)) # raise per request if needed
122
+ temperature = float(params.get("temperature", 0.1))
123
+ top_p = float(params.get("top_p", 1.0))
124
+ top_k: Optional[int] = params.get("top_k", None)
125
+ use_cache = bool(params.get("use_cache", False))
126
+ repetition_penalty = float(params.get("repetition_penalty", 1.0))
127
+ eos_token_id = params.get("eos_token_id", self.tokenizer.eos_token_id)
128
+ pad_token_id = params.get("pad_token_id", self.tokenizer.pad_token_id)
129
+
130
+ inputs = self._build_model_inputs(prompt)
131
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
132
  gen_kwargs = dict(
133
+ **inputs,
134
+ max_new_tokens=max_new_tokens,
135
  temperature=temperature,
136
  top_p=top_p,
137
+ do_sample=True, # streaming-like sampling on
138
+ use_cache=use_cache,
139
+ repetition_penalty=repetition_penalty,
140
+ eos_token_id=eos_token_id,
141
+ pad_token_id=pad_token_id,
142
  )
143
  if top_k is not None:
144
  gen_kwargs["top_k"] = int(top_k)
145
+
 
 
 
 
 
 
 
146
  with torch.no_grad():
147
  output_ids = self.model.generate(**gen_kwargs)
148
+
149
+ # Return ONLY the continuation (like your streamer)
150
+ input_len = inputs["input_ids"].shape[-1]
151
  generated_ids = output_ids[0, input_len:]
152
+ reply_text = self.tokenizer.decode(generated_ids, skip_special_tokens=True)
153
+
154
+ return {
155
+ "reply": [reply_text],
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
156
  "usage": {
157
+ "prompt_tokens": int(input_len),
158
+ "generated_tokens": int(generated_ids.shape[-1]),
 
159
  },
160
  }
161
+
162
+
 
 
163
  if __name__ == "__main__":
164
  h = EndpointHandler(model_dir=".")
165
+ # Raw string
166
+ print(h({"inputs": "How far?", "parameters": {"max_new_tokens": 64}}))
167
+ # Block-style content (your Gradio shape)
168
+ print(h({
169
+ "inputs": [
170
+ {
171
+ "role": "user",
172
+ "content": [{"type": "text", "text": "Explain why sky dey blue."}]
173
+ }
174
  ],
175
+ "parameters": {"max_new_tokens": 64}
176
+ }))
177
+
178
+
 
179
 
180
 
181
  # from typing import Any, Dict, List, Union, Optional