FarmerChat2 / app.py
bhugxer's picture
Fix Gradio example inputs
91fc46c verified
Raw
History Blame Contribute Delete
6.47 kB
"""Gradio Space wrapper for the Copyleft Cultivars Qwen3 v5clean champion.
The model weights live in a separate Hugging Face model repository. Set
MODEL_ID to that repository before starting the Space; the default is the
proposed public model-repository name used by the deployment guide.
"""
from __future__ import annotations
import os
from typing import Any
import gradio as gr
import torch
from transformers import AutoModelForCausalLM, AutoTokenizer
MODEL_ID = os.getenv("MODEL_ID", "CopyleftCultivars/qwen3-v5clean")
MODEL_REVISION = os.getenv("MODEL_REVISION")
HF_TOKEN = os.getenv("HF_TOKEN")
SYSTEM_PROMPT = """You are the Copyleft Cultivars natural-farming assistant.
Give practical, careful answers about Korean Natural Farming, regenerative
agriculture, soil biology, composting, fermentation, crop care, and farm
inputs. Explain assumptions and units. Do not invent measurements, sources,
tool results, or local conditions. When a question depends on local weather,
soil tests, regulations, or a professional diagnosis, say what information is
missing and recommend an appropriate local source or professional.
"""
def _load_model() -> tuple[Any, Any]:
"""Load the pinned model and tokenizer once when the Space starts."""
dtype = torch.float16 if torch.cuda.is_available() else torch.float32
common_kwargs: dict[str, Any] = {
"token": HF_TOKEN,
"torch_dtype": dtype,
"device_map": "auto",
}
if MODEL_REVISION:
common_kwargs["revision"] = MODEL_REVISION
tokenizer = AutoTokenizer.from_pretrained(MODEL_ID, token=HF_TOKEN, revision=MODEL_REVISION)
model = AutoModelForCausalLM.from_pretrained(MODEL_ID, **common_kwargs)
model.eval()
return tokenizer, model
tokenizer, model = _load_model()
MODEL_DEVICE = next(model.parameters()).device
def _message_content(value: Any) -> str:
if isinstance(value, str):
return value
if isinstance(value, dict):
text = value.get("text")
if isinstance(text, str):
return text
return ""
def _normalise_history(history: list[Any] | None) -> list[dict[str, str]]:
"""Accept Gradio message history and ignore unsupported multimodal entries."""
normalised: list[dict[str, str]] = []
for entry in history or []:
if isinstance(entry, dict):
role = entry.get("role")
content = _message_content(entry.get("content"))
if role in {"user", "assistant"} and content:
normalised.append({"role": role, "content": content})
continue
if isinstance(entry, (list, tuple)) and len(entry) >= 2:
user_text = _message_content(entry[0])
assistant_text = _message_content(entry[1])
if user_text:
normalised.append({"role": "user", "content": user_text})
if assistant_text:
normalised.append({"role": "assistant", "content": assistant_text})
return normalised
def _tokenize_messages(messages: list[dict[str, str]], enable_thinking: bool) -> Any:
"""Render Qwen chat messages, retaining compatibility with older Transformers."""
template_kwargs = {
"tokenize": True,
"add_generation_prompt": True,
"return_tensors": "pt",
}
try:
return tokenizer.apply_chat_template(
messages,
enable_thinking=enable_thinking,
**template_kwargs,
)
except TypeError:
return tokenizer.apply_chat_template(messages, **template_kwargs)
def chat(
message: str,
history: list[Any] | None,
enable_thinking: bool,
max_new_tokens: int,
temperature: float,
) -> str:
"""Generate one answer from the v5clean champion."""
user_message = (message or "").strip()
if not user_message:
return "Please enter a question."
messages = [{"role": "system", "content": SYSTEM_PROMPT}]
messages.extend(_normalise_history(history))
messages.append({"role": "user", "content": user_message})
rendered = _tokenize_messages(messages, enable_thinking)
if isinstance(rendered, torch.Tensor):
model_inputs = {"input_ids": rendered}
else:
model_inputs = dict(rendered)
model_inputs = {
key: value.to(MODEL_DEVICE) if hasattr(value, "to") else value
for key, value in model_inputs.items()
}
prompt_length = model_inputs["input_ids"].shape[-1]
generation_kwargs: dict[str, Any] = {
**model_inputs,
"max_new_tokens": int(max_new_tokens),
"pad_token_id": tokenizer.pad_token_id or tokenizer.eos_token_id,
"eos_token_id": tokenizer.eos_token_id,
"do_sample": temperature > 0.01,
}
if temperature > 0.01:
generation_kwargs["temperature"] = float(temperature)
generation_kwargs["top_p"] = 0.9
with torch.inference_mode():
generated = model.generate(**generation_kwargs)
answer_tokens = generated[0, prompt_length:]
answer = tokenizer.decode(answer_tokens, skip_special_tokens=True).strip()
return answer or "The model returned an empty response. Please try again."
thinking_toggle = gr.Checkbox(
value=False,
label="Enable reasoning",
info="Show the model's reasoning mode when supported by the tokenizer.",
)
max_tokens_slider = gr.Slider(
minimum=64,
maximum=2048,
value=512,
step=64,
label="Maximum new tokens",
)
temperature_slider = gr.Slider(
minimum=0.0,
maximum=1.2,
value=0.6,
step=0.1,
label="Temperature",
)
demo = gr.ChatInterface(
fn=chat,
type="messages",
additional_inputs=[thinking_toggle, max_tokens_slider, temperature_slider],
title="Copyleft Cultivars · Qwen3 v5clean",
description=(
"A research preview of the current English v5clean champion for "
"natural-farming questions. Verify recommendations locally before use."
),
examples=[
[
"How can I prepare a small-batch fermented plant juice safely?",
False,
512,
0.6,
],
[
"What information should I collect before diagnosing poor soil drainage?",
False,
512,
0.6,
],
[
"How do Korean Natural Farming inputs differ from ordinary compost tea?",
False,
512,
0.6,
],
],
)
if __name__ == "__main__":
demo.launch()