File size: 7,418 Bytes
faeca74
a870151
1743d89
a870151
 
 
3da7506
a870151
faeca74
a870151
faeca74
 
 
d17912d
3da7506
faeca74
 
 
 
1743d89
 
5353390
b4ddf84
5353390
b4ddf84
 
1743d89
5353390
faeca74
a870151
 
 
88a57d7
 
 
 
 
 
 
 
059cb3e
 
 
 
 
 
 
 
 
 
 
ce95d7d
 
 
 
059cb3e
ce95d7d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
059cb3e
 
 
 
 
 
a870151
faeca74
a870151
 
faeca74
 
a870151
 
faeca74
a870151
 
2a4bbaf
b4ddf84
1743d89
a870151
 
 
 
 
 
faeca74
 
a870151
 
faeca74
e08f2a6
b4ddf84
1743d89
059cb3e
a870151
 
 
b4ddf84
faeca74
 
 
a870151
059cb3e
 
 
 
 
 
 
 
 
 
 
 
 
 
a870151
 
b4ddf84
1743d89
a870151
 
 
 
 
 
 
 
 
1743d89
 
a870151
 
 
 
b4ddf84
 
faeca74
059cb3e
ce95d7d
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
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

@app.get("/")
def home():
    return {"status": "Vex AI is Online"}

@app.post("/chat")
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}"}


@app.post("/chat/stream")
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")