| """ |
| Inkling — a chat UI served by gradio.Server. |
| |
| Backend: gradio.Server (FastAPI + Gradio's queue/concurrency engine). |
| Model: thinkingmachines/Inkling via Hugging Face Inference Providers, |
| reached through the OpenAI-compatible router at |
| https://router.huggingface.co/v1. |
| Frontend: a hand-written single-page chat client (index.html) that talks to |
| the backend through the Gradio JS client, so every request goes |
| through Gradio's queue — including the token stream. |
| """ |
|
|
| import os |
| import sys |
| import traceback |
| from pathlib import Path |
| from typing import Iterator |
|
|
| from fastapi.responses import HTMLResponse |
| from gradio import Server |
| from openai import OpenAI |
|
|
|
|
| |
| |
| |
| |
| |
| |
| def _quiet_unraisable(unraisable_args) -> None: |
| exc_type, exc_value = unraisable_args.exc_type, unraisable_args.exc_value |
| if isinstance(exc_value, ValueError) and "Invalid file descriptor" in str(exc_value): |
| return |
| traceback.print_exception( |
| unraisable_args.exc_type, unraisable_args.exc_value, |
| unraisable_args.exc_traceback, |
| ) |
|
|
|
|
| sys.unraisablehook = _quiet_unraisable |
|
|
| |
| |
| ROUTER_BASE_URL = "https://router.huggingface.co/v1" |
| DEFAULT_MODEL = "thinkingmachines/Inkling" |
|
|
| |
| |
| |
| PROVIDERS = { |
| "auto": ":auto", |
| "cheapest": ":cheapest", |
| "fastest": ":fastest", |
| } |
|
|
| app = Server() |
|
|
| |
| |
| |
| _client: OpenAI | None = None |
|
|
|
|
| def get_client() -> OpenAI: |
| """Return a cached OpenAI client pointed at the HF router.""" |
| global _client |
| token = os.environ.get("HF_TOKEN") |
| if not token: |
| raise RuntimeError( |
| "HF_TOKEN is not set. Add it as a Space secret " |
| "(Settings → Repository secrets → New secret)." |
| ) |
| if _client is None: |
| _client = OpenAI(base_url=ROUTER_BASE_URL, api_key=token) |
| return _client |
|
|
|
|
| def resolve_model(provider: str) -> str: |
| """Turn a short provider key into a fully-suffixed model id.""" |
| suffix = PROVIDERS.get(provider, PROVIDERS["auto"]) |
| return f"{DEFAULT_MODEL}{suffix}" |
|
|
|
|
| @app.api(stream_every=0) |
| def chat( |
| messages: list[dict], |
| provider: str = "auto", |
| system_prompt: str = "", |
| temperature: float = 0.7, |
| max_tokens: int = 2048, |
| ) -> Iterator[str]: |
| """Stream an assistant reply from Inkling, yielding accumulated text. |
| |
| ``messages`` is the conversation so far, each item ``{"role", "content"}`` |
| with role "user" or "assistant". Each yield is the full text accumulated |
| so far, so the frontend can simply replace the bubble contents on every |
| event. |
| """ |
| |
| full: list[dict] = [] |
| if system_prompt and system_prompt.strip(): |
| full.append({"role": "system", "content": system_prompt.strip()}) |
| full.extend(messages) |
|
|
| try: |
| client = get_client() |
| except Exception as exc: |
| yield f"⚠️ {exc}" |
| return |
|
|
| try: |
| stream = client.chat.completions.create( |
| model=resolve_model(provider), |
| messages=full, |
| temperature=temperature, |
| max_tokens=max_tokens, |
| stream=True, |
| ) |
| except Exception as exc: |
| yield f"⚠️ Request failed: {exc}" |
| return |
|
|
| accumulated = "" |
| for chunk in stream: |
| if not chunk.choices: |
| continue |
| delta = chunk.choices[0].delta.content |
| if delta: |
| accumulated += delta |
| yield accumulated |
|
|
| if not accumulated: |
| yield "(The model returned no content.)" |
|
|
|
|
| @app.get("/config") |
| async def config(): |
| """Small endpoint the frontend can poll for model/provider metadata.""" |
| return { |
| "model": DEFAULT_MODEL, |
| "providers": list(PROVIDERS.keys()), |
| "default_provider": "auto", |
| } |
|
|
|
|
| @app.get("/", response_class=HTMLResponse) |
| async def homepage(): |
| """Serve the single-page chat client.""" |
| html_path = Path(__file__).resolve().parent / "index.html" |
| return HTMLResponse(html_path.read_text(encoding="utf-8")) |
|
|
|
|
| if __name__ == "__main__": |
| app.launch(show_error=True) |