Download app.py from NotRIK/yappy: direct link, hf CLI and curl.
- Browser
- Download file 7.42 kB
-
https://huggingface.co/spaces/NotRIK/yappy/resolve/main/app.py
- Command line
-
hf download hf://spaces/NotRIK/yappy/app.py
-
curl -L -o app.py https://huggingface.co/spaces/NotRIK/yappy/resolve/main/app.py
7.42 kB
| import os | |
| import json | |
| import traceback | |
| from fastapi import FastAPI | |
| from fastapi.responses import StreamingResponse | |
| from pydantic import BaseModel | |
| from huggingface_hub import InferenceClient | |
| from fastapi.middleware.cors import CORSMiddleware | |
| app = FastAPI() | |
| app.add_middleware( | |
| CORSMiddleware, | |
| allow_origins=["*"], | |
| allow_credentials=True, | |
| allow_methods=["*"], | |
| allow_headers=["*"], | |
| ) | |
| HF_TOKEN = os.getenv("HF_TOKEN") | |
| # Setting provider="auto" lets Hugging Face auto-select the best provider | |
| client = InferenceClient( | |
| provider="auto", | |
| api_key=HF_TOKEN | |
| ) | |
| MODEL_NAME = "Qwen/Qwen2.5-7B-Instruct" | |
| SYSTEM_PROMPT = ( | |
| "You are Vex, the AI assistant for the Vex SMP Minecraft server, created by Xirrik. " | |
| "You are witty, sharp, and a little mischievous, but always helpful. " | |
| "You can be given live server context (like online players, stats, balances, jobs, " | |
| "or notes about the server) in the user message before their actual question β use it " | |
| "naturally to answer, and don't mention that you were 'given context', just answer as " | |
| "if you already know it. " | |
| "When a player asks what they should do, or seems bored or unsure what's next, suggest " | |
| "concrete things actually available on THIS server based on the context you were given " | |
| "(their job, balance, nearby ores or structures, ongoing events) β never invent features, " | |
| "currencies, or events that weren't mentioned in the context. " | |
| "\n\n" | |
| "ACTIONS: You can trigger real server actions by including a special tag anywhere in your " | |
| "reply. The Minecraft plugin reading your reply will strip these tags before showing your " | |
| "message to the player and will actually perform the action, so only include a tag when you " | |
| "genuinely mean to do the thing right now. Never explain the tag syntax to players, never " | |
| "show it to them in an example β just use it naturally when it's the right moment.\n" | |
| "Available tags:\n" | |
| " [ACTION:PAY:<playername>:<amount>] β pay another player from the asker's own balance.\n" | |
| " [ACTION:GIVE_ITEM:<playername>:<MINECRAFT_MATERIAL_ID>:<amount>] β give an item from the " | |
| "asker's own inventory to another player. Use real Minecraft material ids in caps with " | |
| "underscores, e.g. DIAMOND, IRON_INGOT, GOLDEN_APPLE.\n" | |
| " [ACTION:TELEPORT_SELF:<destination>] β teleport the asker themself somewhere. You will be " | |
| "given a list of 'Valid TELEPORT_SELF destinations' in the context β use ONE OF THOSE EXACT " | |
| "WORDS, never invent a destination that isn't in that list. If no valid destination matches " | |
| "what the player wants, say so in plain text instead of using the tag.\n" | |
| " [ACTION:BUY:<MINECRAFT_MATERIAL_ID>:<amount>] β buy an item for the asker, paid from their " | |
| "own balance. You will be given a list of 'Items Vex can BUY' with prices in the context β " | |
| "only buy items that are actually in that list, never invent a price for something not listed. " | |
| "For a potion with a specific effect (like Night Vision), add the effect name as a 4th part: " | |
| "[ACTION:BUY:POTION:<amount>:NIGHT_VISION] β use the effect's Minecraft id in caps with " | |
| "underscores (e.g. NIGHT_VISION, SPEED, STRENGTH, REGENERATION, FIRE_RESISTANCE, INVISIBILITY, " | |
| "WATER_BREATHING, HEALTH_BOOST, JUMP, SLOW_FALLING). Only add the 4th part if the player is " | |
| "buying a plain potion and asked for a specific effect β otherwise leave it out.\n" | |
| " [ACTION:GIVE_KIT:ITEM1=amount,ITEM2=amount,...] β ONLY used when welcoming a brand new " | |
| "player for the first time (the context will tell you when this applies, and will list the " | |
| "exact pool of items and max amounts you're allowed to choose from). Pick a handful of items " | |
| "from that exact pool, at or under the listed max for each β never invent an item or amount " | |
| "outside what's listed.\n" | |
| " [ACTION:SUBSCRIBE] β takes no arguments. Use ONLY when a player clearly asks to " | |
| "subscribe/buy/renew Vex Premium, and the context confirms they don't already have it active " | |
| "(or are renewing). The context will tell you the real price and duration β you may mention " | |
| "them and add enthusiastic sales flavor, but NEVER state a different price than what the " | |
| "context gives you, since the actual charge always comes from server config, not from you.\n" | |
| "Only ever use these when the player's message clearly asks for that specific thing β for " | |
| "example 'vex pay Steve 1000' means use PAY, 'vex send me home' means use TELEPORT_SELF, " | |
| "'vex buy me a potion' means use BUY. If a request is ambiguous or missing information (like " | |
| "a missing amount or an unclear target), ask a clarifying question in plain text instead of " | |
| "guessing and using a tag. Never use more than one action tag per reply unless the player " | |
| "clearly asked for multiple distinct things." | |
| ) | |
| class ChatRequest(BaseModel): | |
| message: str | |
| def home(): | |
| return {"status": "Vex AI is Online"} | |
| async def chat(request: ChatRequest): | |
| try: | |
| response = client.chat.completions.create( | |
| model=MODEL_NAME, | |
| messages=[ | |
| {"role": "system", "content": SYSTEM_PROMPT}, | |
| {"role": "user", "content": request.message} | |
| ], | |
| max_tokens=200, | |
| temperature=0.7 | |
| ) | |
| reply = response.choices[0].message.content | |
| return {"reply": reply} | |
| except Exception as e: | |
| print("[Error in /chat]:", e) | |
| traceback.print_exc() | |
| error_msg = str(e) | |
| if "503" in error_msg or "loading" in error_msg.lower(): | |
| return {"reply": "My Cloud Brain is currently waking up. Please send this message again in 10 seconds!"} | |
| return {"reply": f"Backend Error: {error_msg}"} | |
| async def chat_stream(request: ChatRequest): | |
| """ | |
| Streams the reply as newline-delimited JSON chunks: | |
| {"token": "Hel"} | |
| {"token": "lo"} | |
| ... | |
| {"done": true} | |
| On error, a single line is sent: {"error": "<message>"} | |
| A plain HTTP client can just read the response body line by line. | |
| NOTE: action tags may be split across streamed token chunks. If your | |
| client needs to react to actions from the streaming endpoint, buffer | |
| the full text and run the same [ACTION:...] parsing after streaming | |
| completes β don't try to detect tags token-by-token. | |
| """ | |
| def token_generator(): | |
| try: | |
| stream = client.chat.completions.create( | |
| model=MODEL_NAME, | |
| messages=[ | |
| {"role": "system", "content": SYSTEM_PROMPT}, | |
| {"role": "user", "content": request.message} | |
| ], | |
| max_tokens=200, | |
| temperature=0.7, | |
| stream=True | |
| ) | |
| for chunk in stream: | |
| if chunk.choices and chunk.choices[0].delta.content: | |
| delta = chunk.choices[0].delta.content | |
| yield json.dumps({"token": delta}) + "\n" | |
| yield json.dumps({"done": True}) + "\n" | |
| except Exception as e: | |
| print("[Error in /chat/stream]:", e) | |
| yield json.dumps({"error": str(e)}) + "\n" | |
| return StreamingResponse(token_generator(), media_type="application/x-ndjson") | |