Spaces:
Paused
Paused
| import base64 | |
| import io | |
| import warnings | |
| # Gradio still references Starlette's deprecated HTTP_422_UNPROCESSABLE_ENTITY. | |
| warnings.filterwarnings( | |
| "ignore", | |
| message="'HTTP_422_UNPROCESSABLE_ENTITY' is deprecated", | |
| category=DeprecationWarning, | |
| ) | |
| # Stale installs or old Space builds may still import duckduckgo_search; we use Brave only. | |
| warnings.filterwarnings( | |
| "ignore", | |
| message=r".*`duckduckgo_search`.*renamed.*`ddgs`.*", | |
| category=RuntimeWarning, | |
| ) | |
| import gradio as gr | |
| from huggingface_hub import InferenceClient | |
| from PIL import Image | |
| from agent import run_agent | |
| from agent.orchestrator import strip_qwen_thinking | |
| MODEL = "Qwen/Qwen3-VL-30B-A3B-Thinking" | |
| DEFAULT_SYSTEM = ( | |
| "You are a helpful, multimodal AI assistant. When an image is sent, simply transcribe it without analysis, unless the user asks for analysis." \ | |
| "When you need up-to-date information from the internet, use the search_web tool. Once you have a complete answer, call final_output with the full answer." \ | |
| "If a task is impossible or unsafe, call abort with a brief reason." | |
| ) | |
| def _image_to_data_url(image_path: str, max_side: int = 1120, quality: int = 85) -> str: | |
| """Resize and base64-encode a local image so the HF payload stays under the limit.""" | |
| with Image.open(image_path) as img: | |
| img = img.convert("RGB") | |
| w, h = img.size | |
| if max(w, h) > max_side: | |
| scale = max_side / max(w, h) | |
| img = img.resize((int(w * scale), int(h * scale)), Image.LANCZOS) | |
| buf = io.BytesIO() | |
| img.save(buf, format="JPEG", quality=quality) | |
| return "data:image/jpeg;base64," + base64.b64encode(buf.getvalue()).decode() | |
| def _normalize_content_for_api(content): | |
| """ | |
| Gradio multimodal history uses {'type': 'file', 'file': FileData}; the HF | |
| chat API expects {'type': 'image_url', 'image_url': {'url': ...}}. | |
| """ | |
| if isinstance(content, str): | |
| return content | |
| if not isinstance(content, list): | |
| return content | |
| out: list = [] | |
| for part in content: | |
| if isinstance(part, str): | |
| out.append({"type": "text", "text": part}) | |
| continue | |
| if not isinstance(part, dict): | |
| continue | |
| ptype = part.get("type") | |
| if ptype == "text": | |
| out.append({"type": "text", "text": part.get("text", "")}) | |
| elif ptype == "file": | |
| fd = part.get("file") | |
| if isinstance(fd, dict) and fd.get("path"): | |
| out.append( | |
| { | |
| "type": "image_url", | |
| "image_url": {"url": _image_to_data_url(fd["path"])}, | |
| } | |
| ) | |
| elif ptype == "image_url": | |
| out.append(part) | |
| if not out: | |
| return "" | |
| if len(out) == 1 and out[0].get("type") == "text": | |
| return out[0].get("text", "") | |
| return out | |
| def _strip_thinking_from_normalized(content): | |
| """Remove Qwen thinking tags from text Gradio stored on assistant turns.""" | |
| if isinstance(content, str): | |
| return strip_qwen_thinking(content) | |
| if isinstance(content, list): | |
| out = [] | |
| for part in content: | |
| if isinstance(part, dict) and part.get("type") == "text": | |
| t = part.get("text", "") | |
| out.append({**part, "text": strip_qwen_thinking(t)}) | |
| else: | |
| out.append(part) | |
| return out | |
| return content | |
| def _normalize_history_message(msg: dict) -> dict: | |
| role = msg.get("role") | |
| if role not in ("user", "assistant", "system"): | |
| return msg | |
| content = msg.get("content") | |
| normalized = _normalize_content_for_api(content) | |
| if role == "assistant": | |
| normalized = _strip_thinking_from_normalized(normalized) | |
| return {**msg, "content": normalized} | |
| def respond( | |
| message, | |
| history: list[dict], | |
| system_message, | |
| max_tokens, | |
| temperature, | |
| top_p, | |
| hf_token: gr.OAuthToken, | |
| ): | |
| client = InferenceClient(token=hf_token.token, model=MODEL) | |
| # multimodal=True sends {"text": str, "files": [path, ...]} | |
| # Guard against plain strings in case of edge-case history replay | |
| if isinstance(message, dict): | |
| text = message.get("text", "") | |
| files = message.get("files", []) | |
| else: | |
| text = message or "" | |
| files = [] | |
| if files: | |
| content = [] | |
| if text: | |
| content.append({"type": "text", "text": text}) | |
| for f in files: | |
| content.append({"type": "image_url", "image_url": {"url": _image_to_data_url(f)}}) | |
| else: | |
| content = text | |
| messages = [{"role": "system", "content": system_message}] | |
| messages.extend(_normalize_history_message(m) for m in history) | |
| messages.append({"role": "user", "content": content}) | |
| answer = run_agent( | |
| messages=messages, | |
| client=client, | |
| model=MODEL, | |
| max_tokens=max_tokens, | |
| temperature=temperature, | |
| top_p=top_p, | |
| ) | |
| partial = "" | |
| for i in range(0, len(answer), 8): | |
| partial += answer[i : i + 8] | |
| yield partial | |
| chatbot = gr.ChatInterface( | |
| respond, | |
| multimodal=True, | |
| chatbot=gr.Chatbot(height=700), | |
| additional_inputs=[ | |
| gr.Textbox(value=DEFAULT_SYSTEM, label="System message"), | |
| gr.Slider(minimum=1, maximum=16384, value=16384, step=1, label="Max new tokens"), | |
| gr.Slider(minimum=0.1, maximum=4.0, value=0.7, step=0.1, label="Temperature"), | |
| gr.Slider(minimum=0.1, maximum=1.0, value=0.95, step=0.05, label="Top-p (nucleus sampling)"), | |
| ], | |
| ) | |
| with gr.Blocks() as demo: | |
| with gr.Sidebar(): | |
| gr.LoginButton() | |
| chatbot.render() | |
| if __name__ == "__main__": | |
| demo.launch(ssr_mode=False) | |