| """Gradio entrypoint for the TuringDNA assistant Space. |
| |
| Two surfaces: |
| |
| 1. Gradio ChatInterface at "/" — for humans visiting |
| huggingface.co/spaces/winter4000/turingdna-assistant directly to |
| sanity-check the model. |
| |
| 2. Auto-generated Gradio API at "/api/predict" (or /run/predict on |
| older Gradio) — what the Flask app's /api/chat proxy calls via |
| gradio_client.Client(...).predict(...). |
| |
| The Flask side uses `api_name="/chat"`, so the ChatInterface event is |
| registered with that api_name. |
| |
| ZeroGPU note: the actual inference is in llm.generate() which carries |
| the @spaces.GPU(duration=60) decorator. From here we just hand control |
| to ChatInterface — Gradio handles the request queueing and ZeroGPU |
| acquires the A100 when generate() is called. |
| """ |
|
|
| import os |
| import sys |
|
|
| import gradio as gr |
| import fastapi |
| import starlette |
| import jinja2 |
|
|
| |
| |
| |
| |
| |
| print( |
| f"[turingdna.assistant] gradio={gr.__version__} fastapi={fastapi.__version__} " |
| f"starlette={starlette.__version__} jinja2={jinja2.__version__} python={sys.version.split()[0]}", |
| flush=True, |
| ) |
|
|
| from llm import chat, MODEL_ID_IN_USE, SYSTEM_PROMPT |
|
|
|
|
| def chat_fn(message, history=None) -> str: |
| """Gradio ChatInterface callback. |
| |
| Deliberately IGNORES Gradio's session `history` — the Flask client |
| embeds conversation context into the message string itself. Single |
| source of truth = the message string. |
| |
| Bulletproof type coercion: Gradio 4.44 has inconsistent type |
| handling depending on call path (UI vs API), version, and theme. |
| We've seen message arrive as: a str, a {'role','content'} dict, a |
| list-of-dicts, and an empty string. Coerce everything to a plain |
| str BEFORE anything else touches it. |
| |
| Also: this entire function is wrapped in a top-level exception |
| handler that logs the traceback to stderr so the next mystery |
| 'list' object has no attribute 'get' tells us which LINE |
| blew up (the previous error format was useless for debugging |
| because it stripped the traceback). |
| """ |
| import sys |
| import traceback |
|
|
| |
| print( |
| f"[chat_fn] in message_type={type(message).__name__} " |
| f"history_type={type(history).__name__} " |
| f"history_len={len(history) if hasattr(history, '__len__') else 'n/a'}", |
| flush=True, |
| ) |
| try: |
| |
| if message is None: |
| text = "" |
| elif isinstance(message, str): |
| text = message |
| elif isinstance(message, dict): |
| |
| text = str(message.get("content") or "") |
| elif isinstance(message, (list, tuple)): |
| |
| if not message: |
| text = "" |
| else: |
| last = message[-1] |
| if isinstance(last, dict): |
| text = str(last.get("content") or "") |
| else: |
| text = str(last) |
| else: |
| text = str(message) |
|
|
| text = text.strip() |
| if not text: |
| return "Ask me a question about your sequence, mutations, or library." |
|
|
| |
| return chat(text, history=[]) |
|
|
| except Exception as exc: |
| |
| |
| |
| tb = traceback.format_exc() |
| print( |
| f"[chat_fn] EXCEPTION {type(exc).__name__}: {exc}\n{tb}", |
| file=sys.stderr, |
| flush=True, |
| ) |
| return f"Generation error: {type(exc).__name__}: {exc}" |
|
|
|
|
| |
| |
| |
| |
| DEMO_PROMPTS = [ |
| "Explain the ESM-2 ΔLL score in one paragraph for someone with a biochem background.", |
| "I have a 280-aa enzyme and the engine is suggesting M1L, V8I, and K42R. Which of these is most likely to affect activity, and why?", |
| "What's the difference between site-saturation mutagenesis and the combinatorial library approach this engine uses?", |
| "Why does codon optimization for E. coli sometimes hurt expression in yeast?", |
| ] |
|
|
|
|
| |
| |
| |
| |
| |
| |
| demo = gr.ChatInterface( |
| fn=chat_fn, |
| type="messages", |
| title="TuringDNA Assistant", |
| description=( |
| f"In-app biology assistant powered by **{MODEL_ID_IN_USE}** on ZeroGPU. " |
| "Ask about protein sequences, ΔLL scores, codon optimization, or cloning strategy. " |
| "First call after the Space wakes up takes 10–30 s while the GPU is acquired." |
| ), |
| examples=DEMO_PROMPTS, |
| cache_examples=False, |
| |
| |
| |
| |
| ) |
|
|
|
|
| if __name__ == "__main__": |
| demo.queue(default_concurrency_limit=1).launch( |
| server_name="0.0.0.0", |
| |
| |
| server_port=int(os.environ.get("GRADIO_SERVER_PORT", "7860")), |
| show_api=True, |
| ) |
|
|