Added a method to detect reasoning models and turn reasoning off.
This commit is contained in:
130
game/llm_ren.py
130
game/llm_ren.py
@@ -11,15 +11,35 @@ init python:
|
|||||||
|
|
||||||
import re
|
import re
|
||||||
|
|
||||||
|
|
||||||
|
EMOTION_REGEX = re.compile(r"EMOTION:\w+")
|
||||||
|
EMOTION_TOKEN_REGEX = re.compile(rf"{EMOTION_REGEX.pattern} ?")
|
||||||
|
|
||||||
|
EMOJI_REGEX = re.compile(
|
||||||
|
"["
|
||||||
|
"\U0001f1e6-\U0001f1ff" # flags
|
||||||
|
"\U0001f300-\U0001f5ff" # symbols and pictographs
|
||||||
|
"\U0001f600-\U0001f64f" # emoticons
|
||||||
|
"\U0001f680-\U0001f6ff" # transport and map
|
||||||
|
"\U0001f900-\U0001f9ff" # supplemental symbols and pictographs
|
||||||
|
"\U0001fa70-\U0001faff" # symbols and pictographs extended
|
||||||
|
"\U00002702-\U000027b0" # dingbats
|
||||||
|
"\U0001f3fb-\U0001f3ff" # skin tone modifiers
|
||||||
|
"\u200d" # zero-width joiner
|
||||||
|
"\ufe0f" # emoji variation selector
|
||||||
|
"]+",
|
||||||
|
flags=re.UNICODE,
|
||||||
|
)
|
||||||
|
|
||||||
EMOTIONS = [
|
EMOTIONS = [
|
||||||
'happy',
|
"happy",
|
||||||
'sad',
|
"sad",
|
||||||
'surprised',
|
"surprised",
|
||||||
'embarrassed',
|
"embarrassed",
|
||||||
'flirty',
|
"flirty",
|
||||||
'angry',
|
"angry",
|
||||||
'thinking',
|
"thinking",
|
||||||
'confused'
|
"confused",
|
||||||
]
|
]
|
||||||
|
|
||||||
SYSTEM_PROMPT = """
|
SYSTEM_PROMPT = """
|
||||||
@@ -54,6 +74,12 @@ EMOTION:happy Hey dummy! Sorry to barge in! Ya feel like hanging out?\n
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def sanitize_speech(text):
|
||||||
|
text_without_emotion_tokens = EMOTION_TOKEN_REGEX.sub("", text)
|
||||||
|
|
||||||
|
return EMOJI_REGEX.sub("", text_without_emotion_tokens)
|
||||||
|
|
||||||
|
|
||||||
def parse_emotion(line):
|
def parse_emotion(line):
|
||||||
def _normalize_emotion(em):
|
def _normalize_emotion(em):
|
||||||
# If not a valid emotion, then search for a match in the
|
# If not a valid emotion, then search for a match in the
|
||||||
@@ -67,14 +93,14 @@ def parse_emotion(line):
|
|||||||
return em
|
return em
|
||||||
|
|
||||||
try:
|
try:
|
||||||
e = re.compile(r'EMOTION:\w+')
|
m = EMOTION_REGEX.match(line)
|
||||||
m = e.match(line)
|
|
||||||
|
|
||||||
if m is not None:
|
if m is not None:
|
||||||
emotion = m.group().split(':')[1]
|
emotion = m.group().split(":")[1]
|
||||||
text = line[m.span()[1]:]
|
text = line[m.span()[1]:]
|
||||||
|
sanitized = sanitize_speech(text)
|
||||||
|
|
||||||
return _normalize_emotion(emotion), text
|
return _normalize_emotion(emotion), sanitized
|
||||||
|
|
||||||
return None, line
|
return None, line
|
||||||
|
|
||||||
@@ -82,34 +108,88 @@ def parse_emotion(line):
|
|||||||
return None, str(e)
|
return None, str(e)
|
||||||
|
|
||||||
|
|
||||||
def sanitize_speech(text):
|
def set_model_capabilities() -> bool:
|
||||||
# This removes all non-ASCII characters (useful for emojis)
|
"""
|
||||||
return text.encode('ascii', 'ignore').decode('ascii')
|
LM Studio throws Bad Request if the reasoning flag is set for a model
|
||||||
|
that doesn't support it. This method tries to determine if the currently
|
||||||
|
configured model supports reasoning to signal to the fetch_llm function
|
||||||
|
disable it.
|
||||||
|
"""
|
||||||
|
|
||||||
|
try:
|
||||||
|
headers = {"Authorization": f"Bearer {persistent.api_key}"}
|
||||||
|
data = {
|
||||||
|
"model": persistent.model,
|
||||||
|
"input": "Start the conversation.",
|
||||||
|
"reasoning": "off",
|
||||||
|
"system_prompt": SYSTEM_PROMPT,
|
||||||
|
}
|
||||||
|
|
||||||
|
renpy.fetch(
|
||||||
|
f"{persistent.base_url}/api/v1/chat",
|
||||||
|
headers=headers,
|
||||||
|
json=data,
|
||||||
|
result="json",
|
||||||
|
)
|
||||||
|
|
||||||
|
except renpy.FetchError as fe:
|
||||||
|
# renpy.fetch returned a BadRequest, assume this means LM Studio
|
||||||
|
# rejected the request because the model doesn't support the
|
||||||
|
# reasoning setting in chat.
|
||||||
|
if hasattr(fe, "status_code") and fe.status_code == 400:
|
||||||
|
persistent.disable_reasoning = False
|
||||||
|
|
||||||
|
return True, None
|
||||||
|
|
||||||
|
else:
|
||||||
|
return False, str(fe)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
# Something else happened.
|
||||||
|
return False, str(e)
|
||||||
|
|
||||||
|
else:
|
||||||
|
# The fetch worked, so the reasoning setting is available.
|
||||||
|
persistent.disable_reasoning = True
|
||||||
|
|
||||||
|
return True, None
|
||||||
|
|
||||||
|
|
||||||
def fetch_llm(message: str) -> str:
|
def fetch_llm(message: str) -> str:
|
||||||
|
"""
|
||||||
|
Queries the chat with a model endpoint of the configured LM Studio server.
|
||||||
|
"""
|
||||||
|
|
||||||
global last_response_id
|
global last_response_id
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Set basic request data.
|
# Set request data.
|
||||||
headers = {"Authorization": f"Bearer {persistent.api_key}"}
|
headers = {"Authorization": f"Bearer {persistent.api_key}"}
|
||||||
data = {"model": persistent.model,
|
data = {
|
||||||
"input": message,
|
"model": persistent.model,
|
||||||
"system_prompt": SYSTEM_PROMPT}
|
"input": message,
|
||||||
|
"system_prompt": SYSTEM_PROMPT,
|
||||||
|
}
|
||||||
|
|
||||||
|
if persistent.disable_reasoning:
|
||||||
|
data["reasoning"] = "off"
|
||||||
|
|
||||||
# Add the previous response ID if any to continue the conversation.
|
# Add the previous response ID if any to continue the conversation.
|
||||||
if last_response_id is not None:
|
if last_response_id is not None:
|
||||||
data["previous_response_id"] = last_response_id
|
data["previous_response_id"] = last_response_id
|
||||||
|
|
||||||
response = renpy.fetch(f"{persistent.base_url}/api/v1/chat",
|
# Fetch from LM Studio and parse the response.
|
||||||
headers=headers,
|
response = renpy.fetch(
|
||||||
json=data,
|
f"{persistent.base_url}/api/v1/chat",
|
||||||
result="json")
|
headers=headers,
|
||||||
|
json=data,
|
||||||
|
result="json",
|
||||||
|
)
|
||||||
|
|
||||||
last_response_id = response["response_id"]
|
last_response_id = response["response_id"]
|
||||||
text = response["output"][0]["content"]
|
text = response["output"][0]["content"]
|
||||||
|
|
||||||
return text.split('\n')
|
return text.split("\n")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return [f'Failed to fetch with error: {e}']
|
return [f"Failed to fetch with error: {e}"]
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ define gui.show_name = True
|
|||||||
|
|
||||||
## The version of the game.
|
## The version of the game.
|
||||||
|
|
||||||
define config.version = "0.2"
|
define config.version = "0.3"
|
||||||
|
|
||||||
|
|
||||||
## Text that is placed on the game's about screen. Place the text between the
|
## Text that is placed on the game's about screen. Place the text between the
|
||||||
@@ -84,17 +84,18 @@ define config.intra_transition = dissolve
|
|||||||
|
|
||||||
## A transition that is used after a game has been loaded.
|
## A transition that is used after a game has been loaded.
|
||||||
|
|
||||||
define config.after_load_transition = None
|
define config.after_load_transition = dissolve
|
||||||
|
|
||||||
|
|
||||||
## Used when entering the main menu after the game has ended.
|
## Used when entering the main menu after the game has ended.
|
||||||
|
|
||||||
define config.end_game_transition = None
|
define config.end_game_transition = dissolve
|
||||||
|
|
||||||
|
|
||||||
## A variable to set the transition used when the game starts does not exist.
|
## A variable to set the transition used when the game starts does not exist.
|
||||||
## Instead, use a with statement after showing the initial scene.
|
## Instead, use a with statement after showing the initial scene.
|
||||||
|
|
||||||
|
define config.end_splash_transition = dissolve
|
||||||
|
|
||||||
## Window management ###########################################################
|
## Window management ###########################################################
|
||||||
##
|
##
|
||||||
@@ -217,3 +218,4 @@ define config.minimum_presplash_time = 2.0
|
|||||||
default persistent.base_url = 'http://localhost:1234'
|
default persistent.base_url = 'http://localhost:1234'
|
||||||
default persistent.api_key = ''
|
default persistent.api_key = ''
|
||||||
default persistent.model = 'gemma-3-4b-it'
|
default persistent.model = 'gemma-3-4b-it'
|
||||||
|
default persistent.disable_reasoning = False
|
||||||
|
|||||||
@@ -1,39 +1,56 @@
|
|||||||
define a = Character("Anita", color = "#aaaa00", callback = speaker("a"), image = "anita")
|
define a = Character("Anita", color = "#aaaa00", callback = speaker("a"), image = "anita")
|
||||||
|
|
||||||
label start:
|
label start:
|
||||||
play music ["zeropage_ambiphonic303chilloutmix.mp3",
|
stop music fadeout 1.0
|
||||||
"zeropage_ambientdance.mp3",
|
scene bg room
|
||||||
"zeropage_ambiose.mp3" ] fadeout 0.5 fadein 0.5
|
with Dissolve(2.0)
|
||||||
|
|
||||||
scene bg room
|
$ success, error = set_model_capabilities()
|
||||||
show anita happy with dissolve
|
|
||||||
|
if not success:
|
||||||
python:
|
call failure(error) from _call_failure
|
||||||
response = fetch_llm('Start the conversation.')[0]
|
return
|
||||||
sanitized = sanitize_speech(response)
|
|
||||||
emotion, line = parse_emotion(sanitized)
|
play music ["zeropage_ambiphonic303chilloutmix.mp3",
|
||||||
|
"zeropage_ambientdance.mp3",
|
||||||
a "[line]"
|
"zeropage_ambiose.mp3" ] fadeout 0.5 fadein 0.5
|
||||||
|
|
||||||
while True:
|
show anita happy with dissolve
|
||||||
python:
|
|
||||||
message = renpy.input(prompt = "What do you say to her?")
|
python:
|
||||||
response = fetch_llm(message)
|
response = fetch_llm('Start the conversation.')[0]
|
||||||
i = 0
|
emotion, line = parse_emotion(response)
|
||||||
|
|
||||||
while i < len(response):
|
a "[line]"
|
||||||
python:
|
|
||||||
r = response[i].strip()
|
while True:
|
||||||
s = sanitize_speech(r)
|
python:
|
||||||
|
message = renpy.input(prompt = "What do you say to her?")
|
||||||
if s != '':
|
response = fetch_llm(message)
|
||||||
$ emotion, line = parse_emotion(s)
|
i = 0
|
||||||
|
|
||||||
if emotion is not None:
|
while i < len(response):
|
||||||
show expression f'anita {emotion}'
|
python:
|
||||||
|
r = response[i].strip()
|
||||||
a "[line]"
|
|
||||||
|
if r != '':
|
||||||
$ i += 1
|
$ emotion, line = parse_emotion(r)
|
||||||
|
|
||||||
return
|
if emotion is not None:
|
||||||
|
show expression f'anita {emotion}'
|
||||||
|
|
||||||
|
a "[line]"
|
||||||
|
|
||||||
|
$ i += 1
|
||||||
|
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
|
label failure(error):
|
||||||
|
"""Alas! Figuring out the capabilities of the configured model failed with the following error.
|
||||||
|
|
||||||
|
[error]
|
||||||
|
|
||||||
|
Unfortunately the program cannot continue, returning to the main menu."""
|
||||||
|
|
||||||
|
return
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
{
|
{
|
||||||
"build_update": false,
|
"build_update": false,
|
||||||
"packages": [
|
"packages": [
|
||||||
"win",
|
|
||||||
"linux",
|
"linux",
|
||||||
"mac"
|
"mac",
|
||||||
|
"win"
|
||||||
],
|
],
|
||||||
"add_from": true,
|
"add_from": true,
|
||||||
"force_recompile": true,
|
"force_recompile": true,
|
||||||
|
|||||||
Reference in New Issue
Block a user