1
1
mirror of https://github.com/leon-ai/leon.git synced 2024-11-27 16:16:48 +03:00

feat(skill/akinator): backbone

This commit is contained in:
louistiti 2022-07-04 00:47:10 +08:00
parent aad9f3f1ef
commit 02a2f71470
No known key found for this signature in database
GPG Key ID: 7ECA3DD523793FE6
15 changed files with 603 additions and 0 deletions

View File

@ -1,5 +1,20 @@
{
"endpoints": [
{
"method": "GET",
"route": "/api/action/games/akinator/start",
"params": []
},
{
"method": "GET",
"route": "/api/action/games/akinator/play",
"params": []
},
{
"method": "GET",
"route": "/api/action/games/akinator/rematch",
"params": []
},
{
"method": "GET",
"route": "/api/action/games/guess_the_number/setup",

View File

View File

View File

@ -0,0 +1,83 @@
{
"actions": {
"start": {
"type": "dialog",
"utterance_samples": [
"[Let's play|I want to play|I wanna play] with Akinator"
],
"answers": [
"Alright, I'm calling my friend Akinator..."
],
"next_action": "guess"
},
"guess": {
"type": "logic",
"loop": {
"expected_item": {
"type": "resolver",
"name": "answer"
}
},
"suggestions": [
"Yes",
"No",
"I don't know",
"Probably",
"Probably not"
],
"next_action": "replay"
},
"replay": {
"type": "logic",
"loop": {
"expected_item": {
"type": "resolver",
"name": "affirmation_denial"
}
}
}
},
"resolvers": {
"answer": {
"intents": {
"yes": {
"utterance_samples": [
"[Yes|Yep|Yup|Yeah]",
"Sure",
"Correct"
],
"value": "yes"
},
"no": {
"utterance_samples": [
"[No|Nope|Nah]"
],
"value": "no"
},
"idk": {
"utterance_samples": [
"I do not know",
"Don't know",
"Not sure"
],
"value": "idk"
},
"probably": {
"utterance_samples": [
"Probably",
"Probably yes"
],
"value": "probably"
},
"probably_not": {
"utterance_samples": [
"Probably [no|not]"
],
"value": "probably_not"
}
}
}
},
"answers": {
}
}

View File

@ -0,0 +1,11 @@
{
"name": "Akinator",
"bridge": "python",
"version": "1.0.0",
"description": "Leon's friend, the Akinator genie will read your mind.",
"author": {
"name": "Louis Grenard",
"email": "louis.grenard@gmail.com",
"url": "https://github.com/louistiti"
}
}

View File

@ -0,0 +1,29 @@
#!/usr/bin/env python
# -*- coding:utf-8 -*-
import random
import utils
from ..lib import akinator
def guess(params):
aki = akinator.Akinator()
q = aki.start_game()
while aki.progression <= 80:
a = input(q + "\n\t")
if a == "b":
try:
q = aki.back()
except akinator.CantGoBackAnyFurther:
pass
else:
q = aki.answer(a)
aki.win()
correct = input(f"It's {aki.first_guess['name']} ({aki.first_guess['description']})! Was I correct?\n{aki.first_guess['absolute_picture_path']}\n\t")
if correct.lower() == "yes" or correct.lower() == "y":
print("Yay\n")
else:
print("Oof\n")

View File

@ -0,0 +1,7 @@
#!/usr/bin/env python
# -*- coding:utf-8 -*-
import utils
def replay(params):
"""TODO"""

View File

@ -0,0 +1,6 @@
{
"configurations": {
"options": {},
"credentials": {}
}
}

View File

View File

@ -0,0 +1 @@
5.0.0

View File

@ -0,0 +1,33 @@
"""
An API wrapper for the online game, Akinator, written in Python
"""
"""
MIT License
Copyright (c) 2019 NinjaSnail1080
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
"""
from .akinator import Akinator
from .exceptions import *
import os
with open(os.path.join(os.path.dirname(__file__), "VERSION.txt")) as f:
__version__ = f.read()

View File

@ -0,0 +1,231 @@
"""
MIT License
Copyright (c) 2019 NinjaSnail1080
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
"""
import utils
import exceptions
import re
import time
import json
try:
import requests
except ImportError:
pass
#* URLs for the API requests
NEW_SESSION_URL = "https://{}/new_session?callback=jQuery331023608747682107778_{}&urlApiWs={}&partner=1&childMod={}&player=website-desktop&uid_ext_session={}&frontaddr={}&constraint=ETAT<>'AV'&soft_constraint={}&question_filter={}"
ANSWER_URL = "https://{}/answer_api?callback=jQuery331023608747682107778_{}&urlApiWs={}&childMod={}&session={}&signature={}&step={}&answer={}&frontaddr={}&question_filter={}"
BACK_URL = "{}/cancel_answer?callback=jQuery331023608747682107778_{}&childMod={}&session={}&signature={}&step={}&answer=-1&question_filter={}"
WIN_URL = "{}/list?callback=jQuery331023608747682107778_{}&childMod={}&session={}&signature={}&step={}"
#* HTTP headers to use for the requests
HEADERS = {
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8",
"Accept-Encoding": "gzip, deflate",
"Accept-Language": "en-US,en;q=0.9",
"User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) snap Chromium/81.0.4044.92 Chrome/81.0.4044.92 Safari/537.36",
"x-requested-with": "XMLHttpRequest",
}
class Akinator():
"""A class that represents an Akinator game.
The first thing you want to do after calling an instance of this class is to call "start_game()".
"""
def __init__(self):
self.uri = None
self.server = None
self.session = None
self.signature = None
self.uid = None
self.frontaddr = None
self.child_mode = None
self.question_filter = None
self.timestamp = None
self.question = None
self.progression = None
self.step = None
self.first_guess = None
self.guesses = None
def _update(self, resp, start=False):
"""Update class variables"""
if start:
self.session = int(resp["parameters"]["identification"]["session"])
self.signature = int(resp["parameters"]["identification"]["signature"])
self.question = str(resp["parameters"]["step_information"]["question"])
self.progression = float(resp["parameters"]["step_information"]["progression"])
self.step = int(resp["parameters"]["step_information"]["step"])
else:
self.question = str(resp["parameters"]["question"])
self.progression = float(resp["parameters"]["progression"])
self.step = int(resp["parameters"]["step"])
def _parse_response(self, response):
"""Parse the JSON response and turn it into a Python object"""
return json.loads(",".join(response.split("(")[1::])[:-1])
def _get_session_info(self):
"""Get uid and frontaddr from akinator.com/game"""
info_regex = re.compile("var uid_ext_session = '(.*)'\\;\\n.*var frontaddr = '(.*)'\\;")
r = requests.get("https://en.akinator.com/game")
match = info_regex.search(r.text)
self.uid, self.frontaddr = match.groups()[0], match.groups()[1]
def _auto_get_region(self, lang, theme):
"""Automatically get the uri and server from akinator.com for the specified language and theme"""
server_regex = re.compile("[{\"translated_theme_name\":\"[\s\S]*\",\"urlWs\":\"https:\\\/\\\/srv[0-9]+\.akinator\.com:[0-9]+\\\/ws\",\"subject_id\":\"[0-9]+\"}]")
uri = lang + ".akinator.com"
bad_list = ["https://srv12.akinator.com:9398/ws"]
while True:
r = requests.get("https://" + uri)
match = server_regex.search(r.text)
parsed = json.loads(match.group().split("'arrUrlThemesToPlay', ")[-1])
if theme == "c":
server = next((i for i in parsed if i["subject_id"] == "1"), None)["urlWs"]
elif theme == "a":
server = next((i for i in parsed if i["subject_id"] == "14"), None)["urlWs"]
elif theme == "o":
server = next((i for i in parsed if i["subject_id"] == "2"), None)["urlWs"]
if server not in bad_list:
return {"uri": uri, "server": server}
def start_game(self, language=None, child_mode=False):
"""Start an Akinator game. Run this function first before the others. Returns a string containing the first question
The "language" parameter can be left as None for English, the default language, or it can be set to one of the following (case-insensitive):
- "en": English (default)
- "en_animals": English server for guessing animals
- "en_objects": English server for guessing objects
- "ar": Arabic
- "cn": Chinese
- "de": German
- "de_animals": German server for guessing animals
- "es": Spanish
- "es_animals": Spanish server for guessing animals
- "fr": French
- "fr_animals": French server for guessing animals
- "fr_objects": French server for guessing objects
- "il": Hebrew
- "it": Italian
- "it_animals": Italian server for guessing animals
- "jp": Japanese
- "jp_animals": Japanese server for guessing animals
- "kr": Korean
- "nl": Dutch
- "pl": Polish
- "pt": Portuguese
- "ru": Russian
- "tr": Turkish
- "id": Indonesian
You can also put the name of the language spelled out, like "spanish", "korean", "french_animals", etc.
The "child_mode" parameter is False by default. If it's set to True, then Akinator won't ask questions about things that are NSFW
"""
self.timestamp = time.time()
region_info = self._auto_get_region(utils.get_lang_and_theme(language)["lang"], utils.get_lang_and_theme(language)["theme"])
self.uri, self.server = region_info["uri"], region_info["server"]
self.child_mode = child_mode
soft_constraint = "ETAT%3D%27EN%27" if self.child_mode else ""
self.question_filter = "cat%3D1" if self.child_mode else ""
self._get_session_info()
r = requests.get(NEW_SESSION_URL.format(self.uri, self.timestamp, self.server, str(self.child_mode).lower(), self.uid, self.frontaddr, soft_constraint, self.question_filter), headers=HEADERS)
resp = self._parse_response(r.text)
if resp["completion"] == "OK":
self._update(resp, True)
return self.question
else:
return raise_connection_error(resp["completion"])
def answer(self, ans):
"""Answer the current question, which you can find with "Akinator.question". Returns a string containing the next question
The "ans" parameter must be one of these (case-insensitive):
- "yes" OR "y" OR "0" for YES
- "no" OR "n" OR "1" for NO
- "i" OR "idk" OR "i dont know" OR "i don't know" OR "2" for I DON'T KNOW
- "probably" OR "p" OR "3" for PROBABLY
- "probably not" OR "pn" OR "4" for PROBABLY NOT
"""
ans = utils.ans_to_id(ans)
r = requests.get(ANSWER_URL.format(self.uri, self.timestamp, self.server, str(self.child_mode).lower(), self.session, self.signature, self.step, ans, self.frontaddr, self.question_filter), headers=HEADERS)
resp = self._parse_response(r.text)
if resp["completion"] == "OK":
self._update(resp)
return self.question
else:
return utils.raise_connection_error(resp["completion"])
def back(self):
"""Goes back to the previous question. Returns a string containing that question
If you're on the first question and you try to go back again, the CantGoBackAnyFurther exception will be raised
"""
if self.step == 0:
raise exceptions.CantGoBackAnyFurther("You were on the first question and couldn't go back any further")
r = requests.get(BACK_URL.format(self.server, self.timestamp, str(self.child_mode).lower(), self.session, self.signature, self.step, self.question_filter), headers=HEADERS)
resp = self._parse_response(r.text)
if resp["completion"] == "OK":
self._update(resp)
return self.question
else:
return utils.raise_connection_error(resp["completion"])
def win(self):
"""Get Aki's guesses for who the person you're thinking of is based on your answers to the questions so far
Defines and returns the variable "Akinator.first_guess", a dictionary describing his first choice for who you're thinking about. The three most important values in the dict are "name" (character's name), "description" (description of character), and "absolute_picture_path" (direct link to image of character)
This function also defines "Akinator.guesses", which is a list of dictionaries containing his choices in order from most likely to least likely
It's recommended that you call this function when Aki's progression is above 85%, which is when he will have most likely narrowed it down to just one choice. You can get his current progression via "Akinator.progression"
"""
r = requests.get(WIN_URL.format(self.server, self.timestamp, str(self.child_mode).lower(), self.session, self.signature, self.step), headers=HEADERS)
resp = self._parse_response(r.text)
if resp["completion"] == "OK":
self.first_guess = resp["parameters"]["elements"][0]["element"]
self.guesses = [g["element"] for g in resp["parameters"]["elements"]]
return self.first_guess
else:
return utils.raise_connection_error(resp["completion"])

View File

@ -0,0 +1,63 @@
"""
MIT License
Copyright (c) 2019 NinjaSnail1080
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
"""
class InvalidAnswerError(ValueError):
"""Raised when the user inputs an invalid answer"""
pass
class InvalidLanguageError(ValueError):
"""Raised when the user inputs an invalid language"""
pass
class AkiConnectionFailure(Exception):
"""Raised if the Akinator API fails to connect for some reason. Base class for AkiTimedOut, AkiNoQuestions, AkiServerDown, and AkiTechnicalError"""
pass
class AkiTimedOut(AkiConnectionFailure):
"""Raised if the Akinator session times out. Derived from AkiConnectionFailure"""
pass
class AkiNoQuestions(AkiConnectionFailure):
"""Raised if the Akinator API runs out of questions to ask. This will happen if "Akinator.step" is at 79 and the "answer" function is called again. Derived from AkiConnectionFailure"""
pass
class AkiServerDown(AkiConnectionFailure):
"""Raised if Akinator's servers are down for the region you're running on. If this happens, try again later or use a different language. Derived from AkiConnectionFailure"""
pass
class AkiTechnicalError(AkiConnectionFailure):
"""Raised if Aki's servers had a technical error. If this happens, try again later or use a different language. Derived from AkiConnectionFailure"""
pass
class CantGoBackAnyFurther(Exception):
"""Raised when the user is on the first question and tries to go back further"""
pass

View File

@ -0,0 +1,124 @@
"""
MIT License
Copyright (c) 2019 NinjaSnail1080
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
"""
import exceptions
def ans_to_id(ans):
"""Convert an input answer string into an Answer ID for Akinator"""
ans = str(ans).lower()
if ans == "yes" or ans == "y" or ans == "0":
return "0"
elif ans == "no" or ans == "n" or ans == "1":
return "1"
elif ans == "i" or ans == "idk" or ans == "i dont know" or ans == "i don't know" or ans == "2":
return "2"
elif ans == "probably" or ans == "p" or ans == "3":
return "3"
elif ans == "probably not" or ans == "pn" or ans == "4":
return "4"
else:
raise exceptions.InvalidAnswerError("""
You put "{}", which is an invalid answer.
The answer must be one of these:
- "yes" OR "y" OR "0" for YES
- "no" OR "n" OR "1" for NO
- "i" OR "idk" OR "i dont know" OR "i don't know" OR "2" for I DON'T KNOW
- "probably" OR "p" OR "3" for PROBABLY
- "probably not" OR "pn" OR "4" for PROBABLY NOT
""".format(ans))
def get_lang_and_theme(lang=None):
"""Returns the language code and theme based on what is input"""
if lang is None:
return {"lang": "en", "theme": "c"}
lang = str(lang).lower()
if lang == "en" or lang == "english":
return {"lang": "en", "theme": "c"}
elif lang == "en_animals" or lang == "english_animals":
return {"lang": "en", "theme": "a"}
elif lang == "en_objects" or lang == "english_objects":
return {"lang": "en", "theme": "o"}
elif lang == "ar" or lang == "arabic":
return {"lang": "ar", "theme": "c"}
elif lang == "cn" or lang == "chinese":
return {"lang": "cn", "theme": "c"}
elif lang == "de" or lang == "german":
return {"lang": "de", "theme": "c"}
elif lang == "de_animals" or lang == "german_animals":
return {"lang": "de", "theme": "a"}
elif lang == "es" or lang == "spanish":
return {"lang": "es", "theme": "c"}
elif lang == "es_animals" or lang == "spanish_animals":
return {"lang": "es", "theme": "a"}
elif lang == "fr" or lang == "french":
return {"lang": "fr", "theme": "c"}
elif lang == "fr_animals" or lang == "french_animals":
return {"lang": "fr", "theme": "a"}
elif lang == "fr_objects" or lang == "french_objects":
return {"lang": "fr", "theme": "o"}
elif lang == "il" or lang == "hebrew":
return {"lang": "il", "theme": "c"}
elif lang == "it" or lang == "italian":
return {"lang": "it", "theme": "c"}
elif lang == "it_animals" or lang == "italian_animals":
return {"lang": "it", "theme": "a"}
elif lang == "jp" or lang == "japanese":
return {"lang": "jp", "theme": "c"}
elif lang == "jp_animals" or lang == "japanese_animals":
return {"lang": "jp", "theme": "a"}
elif lang == "kr" or lang == "korean":
return {"lang": "kr", "theme": "c"}
elif lang == "nl" or lang == "dutch":
return {"lang": "nl", "theme": "c"}
elif lang == "pl" or lang == "polish":
return {"lang": "pl", "theme": "c"}
elif lang == "pt" or lang == "portuguese":
return {"lang": "pt", "theme": "c"}
elif lang == "ru" or lang == "russian":
return {"lang": "ru", "theme": "c"}
elif lang == "tr" or lang == "turkish":
return {"lang": "tr", "theme": "c"}
elif lang == "id" or lang == "indonesian":
return {"lang": "id", "theme": "c"}
else:
raise exceptions.InvalidLanguageError("You put \"{}\", which is an invalid language.".format(lang))
def raise_connection_error(response):
"""Raise the proper error if the API failed to connect"""
if response == "KO - SERVER DOWN":
raise exception.AkiServerDown("Akinator's servers are down in this region. Try again later or use a different language")
elif response == "KO - TECHNICAL ERROR":
raise exception.AkiTechnicalError("Akinator's servers have had a technical error. Try again later or use a different language")
elif response == "KO - TIMEOUT":
raise exceptions.AkiTimedOut("Your Akinator session has timed out")
elif response == "KO - ELEM LIST IS EMPTY" or response == "WARN - NO QUESTION":
raise exceptions.AkiNoQuestions("\"Akinator.step\" reached 79. No more questions")
else:
raise exceptions.AkiConnectionFailure("An unknown error has occured. Server response: {}".format(response))

View File