# -*- coding: utf-8 -*- """AgentScope chat — Gradio app for Hugging Face Spaces (ZeroGPU). This is a single-file Gradio app that runs an AgentScope ``Agent`` in-process. No FastAPI, no React build — just Gradio + AgentScope. That makes it eligible for HF Spaces' free ``zero-a10g`` hardware (which requires the Gradio SDK). Configuration is via HF Spaces **Secrets** (or local env vars): OPENAI_API_KEY — required, your OpenAI (or compatible) key OPENAI_BASE_URL — optional, for OpenAI-compatible endpoints MODEL_NAME — optional, defaults to "gpt-4o-mini" SYSTEM_PROMPT — optional, defaults to a helpful-assistant prompt AGENT_NAME — optional, defaults to "AgentScope" WORKDIR — optional, defaults to /tmp/agentscope-workdir Usage on HF Spaces: 1. Create a Space with SDK = Gradio 2. Set hardware = zero-a10g (free for Gradio Spaces) 3. Add OPENAI_API_KEY as a Secret (and optionally OPENAI_BASE_URL, MODEL_NAME, SYSTEM_PROMPT) 4. Push this app.py + requirements.txt + README.md 5. The Space builds and runs on port 7860 automatically. """ from __future__ import annotations import asyncio import os import sys import traceback from pathlib import Path from typing import Any import gradio as gr # AgentScope — installed from the local source via requirements.txt from agentscope.agent import Agent from agentscope.credential import OpenAICredential from agentscope.message import Msg, TextBlock from agentscope.model import OpenAIChatModel from agentscope.tool import ( Toolkit, Bash, Read, Write, Edit, Glob, Grep, ) # --------------------------------------------------------------------------- # Configuration (HF Secrets / env vars) # --------------------------------------------------------------------------- OPENAI_API_KEY = os.environ.get("OPENAI_API_KEY", "") OPENAI_BASE_URL = os.environ.get("OPENAI_BASE_URL", "") or None MODEL_NAME = os.environ.get("MODEL_NAME", "gpt-4o-mini") SYSTEM_PROMPT = os.environ.get( "SYSTEM_PROMPT", ( "You're AgentScope, a helpful AI assistant with access to a " "workspace directory. You can use the bash, read, write, edit, " "glob, and grep tools to help the user with coding, file " "operations, and research tasks. Always explain what you're " "doing before calling tools." ), ) AGENT_NAME = os.environ.get("AGENT_NAME", "AgentScope") WORKDIR = os.environ.get("WORKDIR", "/tmp/agentscope-workdir") # Ensure the workdir exists so the Bash tool has a CWD Path(WORKDIR).mkdir(parents=True, exist_ok=True) # --------------------------------------------------------------------------- # Build the agent once at module load (HF Spaces reuses the process) # --------------------------------------------------------------------------- def _build_agent() -> Agent | None: """Build the AgentScope agent. Returns None if no API key is set.""" if not OPENAI_API_KEY: return None credential = OpenAICredential( api_key=OPENAI_API_KEY, base_url=OPENAI_BASE_URL, ) model = OpenAIChatModel( credential=credential, model=MODEL_NAME, stream=True, context_size=128_000, parameters=OpenAIChatModel.Parameters( temperature=0.7, ), ) # Built-in agent tools operating on WORKDIR. LocalBackend is the # default backend; we pass cwd=WORKDIR to Bash so commands run there. toolkit = Toolkit( tools=[ Bash(cwd=WORKDIR), Read(), Write(), Edit(), Glob(), Grep(), ], ) return Agent( name=AGENT_NAME, system_prompt=SYSTEM_PROMPT, model=model, toolkit=toolkit, ) _agent: Agent | None = None try: _agent = _build_agent() except Exception as exc: # pragma: no cover print(f"[app] Failed to build agent: {exc}", file=sys.stderr) traceback.print_exc(file=sys.stderr) # --------------------------------------------------------------------------- # Chat handler — bridges Gradio's chat interface with AgentScope # --------------------------------------------------------------------------- def _extract_text(msg: Msg) -> str: """Pull visible text out of an AgentScope Msg (skips tool blocks).""" parts: list[str] = [] for block in msg.content: if hasattr(block, "text") and block.type == "text": parts.append(block.text) return "\n".join(parts) if parts else "" async def _reply(message: str, history: list) -> str: """Run the agent on a single user turn and return the assistant text.""" if _agent is None: return ( "⚠️ No `OPENAI_API_KEY` set. Add it as a Secret in the Space " "settings → Restart the Space → try again." ) user_msg = Msg( name="user", content=[TextBlock(text=message)], role="user", ) # Agent.reply is async — runs the full ReAct loop and returns the # final assistant message (tool calls happen internally). final_msg = await _agent.reply(inputs=user_msg) return _extract_text(final_msg) or "(no response)" def chat(message: str, history: list[dict[str, str]]) -> Any: """Gradio chat handler (sync wrapper around the async reply).""" if not message.strip(): return "", history try: reply_text = asyncio.run(_reply(message, history)) except RuntimeError: # Event loop already running (shouldn't happen in Gradio sync # handler, but fall back gracefully if it does). loop = asyncio.new_event_loop() try: reply_text = loop.run_until_complete(_reply(message, history)) finally: loop.close() except Exception as exc: reply_text = f"❌ Error: {exc}\n```\n{traceback.format_exc()}\n```" history.append({"role": "user", "content": message}) history.append({"role": "assistant", "content": reply_text}) return "", history # --------------------------------------------------------------------------- # Gradio UI # --------------------------------------------------------------------------- CSS = """ .gradio-container { max-width: 900px !important; margin: auto; } .footer { text-align: center; color: #888; font-size: 12px; margin-top: 16px; } .status-ok { color: #16a34a; font-weight: 600; } .status-bad { color: #dc2626; font-weight: 600; } """ with gr.Blocks( title="AgentScope Chat", theme=gr.themes.Soft(primary_hue="indigo"), css=CSS, ) as demo: gr.Markdown( "# 🤖 AgentScope Chat\n" "Multi-agent platform with built-in tools " "(bash, read, write, edit, glob, grep)." ) if _agent is None: gr.Markdown( "### ⚠️ Setup required\n" "No `OPENAI_API_KEY` found. To enable chat:\n" "1. Open this Space's **Settings** tab\n" "2. Scroll to **Variables and secrets** → **New secret**\n" "3. Add `OPENAI_API_KEY` with your key " "(optionally `OPENAI_BASE_URL`, `MODEL_NAME`, `SYSTEM_PROMPT`)\n" "4. Click **Restart Space**\n\n" "Supports OpenAI and any OpenAI-compatible endpoint " "(set `OPENAI_BASE_URL` to your provider's `/v1` URL)." ) else: gr.Markdown( f"**Status:** ● Connected " f"**Model:** `{MODEL_NAME}` " f"**Endpoint:** `{OPENAI_BASE_URL or 'https://api.openai.com/v1'}`" ) gr.ChatInterface( fn=chat, type="messages", chatbot=gr.Chatbot(height=520, type="messages"), textbox=gr.Textbox( placeholder="Ask me anything — I can run bash, edit files, " "search code, and more...", container=False, scale=7, ), submit_btn="Send", stop_btn="Stop", retry_btn="Retry", undo_btn="Undo", clear_btn="Clear", examples=[ "What tools do you have?", "Create a hello.py file with a print statement, then run it.", "Find all .py files in /tmp and count lines in each.", "Explain how MCP (Model Context Protocol) works.", ], ) gr.Markdown( "
" ) # HF Spaces runs `python app.py` and expects the app on port 7860 if __name__ == "__main__": demo.launch(server_name="0.0.0.0", server_port=7860, show_error=True)