From 32d41d4a116d3d702756dd6b0b6f4dcda571196c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Hopst=C3=A4tter?= Date: Fri, 17 May 2024 11:37:40 +0200 Subject: [PATCH 01/10] fix renaming wingmen --- services/config_manager.py | 28 ++++++++++++++++++++++------ 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/services/config_manager.py b/services/config_manager.py index 9101198c..4342e059 100644 --- a/services/config_manager.py +++ b/services/config_manager.py @@ -192,7 +192,7 @@ def get_template_dirs(self) -> list[ConfigDirInfo]: def __get_template_dir(self, config_dir: ConfigDirInfo) -> Optional[ConfigDirInfo]: """Gets the template directory for a given config directory.""" - template_dir = path.join(self.templates_dir, config_dir.directory) + template_dir = path.join(self.templates_dir, CONFIGS_DIR, config_dir.directory) if not path.exists(template_dir): # check if "defaulted" template dir exists default_template_dir = path.join( @@ -221,16 +221,19 @@ def __get_template( return (None, None) for root, dirs, files in walk( - path.join(self.templates_dir, config_dir.directory) + path.join(self.templates_dir, CONFIGS_DIR, config_dir.directory) ): for filename in files: # templates are never logically deleted base_file_name = filename.replace(".template", "") - if filename.endswith( - "template.yaml" + if ( + filename.endswith("template.yaml") # but the given wingman config might be logically deleted - ) and base_file_name == wingman_file.file.replace( - DELETED_PREFIX, "", 1 + and base_file_name == wingman_file.file + or ( + wingman_file.file.startswith(DELETED_PREFIX) + and base_file_name == wingman_file.file[1:] + ) ): file_info = WingmanConfigFileInfo( file=base_file_name, @@ -528,6 +531,19 @@ def save_wingman_config( self.config_dir, config_dir.directory, wingman_file.file ) + # check if there is a template for the old name + tpl, wng = self.__get_template(config_dir, wingman_file) + if tpl and wng: + # leave a .[OLD] file so that it won't be recreated next time + shutil.copyfile( + old_config_path, + path.join( + self.config_dir, + config_dir.directory, + f"{DELETED_PREFIX}{wng.file}", + ), + ) + # move the config shutil.move( old_config_path, From 5526a3591c3a28f2288735a8ad6c60efd49eee97 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Hopst=C3=A4tter?= Date: Fri, 17 May 2024 14:32:55 +0200 Subject: [PATCH 02/10] add Starhead skill to Computer template --- .../_Star Citizen/Computer.template.yaml | 53 ++++++++++++++++--- 1 file changed, 47 insertions(+), 6 deletions(-) diff --git a/templates/configs/_Star Citizen/Computer.template.yaml b/templates/configs/_Star Citizen/Computer.template.yaml index faff9da8..9c666163 100644 --- a/templates/configs/_Star Citizen/Computer.template.yaml +++ b/templates/configs/_Star Citizen/Computer.template.yaml @@ -12,6 +12,47 @@ record_key: end is_voice_activation_default: True sound: effects: [ROBOT] +skills: + - name: StarHead + module: skills.star_head.main + description: + en: Use the StarHead API to retrieve detailed information about spaceships, weapons and more. StarHead can also calculate optimal trading routes based on live data. + de: Nutze die StarHead API, um detaillierte Informationen über Raumschiffe, Waffen und mehr abzurufen. StarHead kann auch optimale Handelsrouten anhand von Live-Daten berechnen. + examples: + - question: + en: I want to trade. What's the best route? + de: Ich möchte handeln. Was ist die beste Route? + answer: + en: To provide you with the best trading route, I need to know your ship model, your current location, and your available budget. Could you please provide these details? + de: Um dir die beste Handelsroute anbieten zu können, muss ich dein Schiffsmodell, deinen aktuellen Standort und dein verfügbares Budget kennen. Kannst du mir diese Angaben bitte mitteilen? + - question: + en: I'm flying a Caterpillar and am near Yela. I have 100.000 credits to spend. + de: Ich fliege eine Caterpillar und bin in der Nähe von Yela. Ich habe 100.000 Credits auszugeben. + answer: + en: You can buy Stims at Deakins Research Outpost near Yela for 2.8 credits/unit and sell them at CRU-L1 Ambitious Dream Station for 3.85 credits/unit. The total profit for this route is approximately 37499 credits, and the travel time estimation is 41 minutes. + de: Du kannst Stims bei Deakins Research Outpost in der Nähe von Yela für 2,8 Credits/Stück kaufen und sie bei CRU-L1 Ambitious Dream Station für 3,85 Credits/Stück verkaufen. Der Gesamtgewinn für diese Route beträgt ca. 37499 Credits, und die geschätzte Reisezeit beträgt 41 Minuten. + - question: + en: What can you tell me about the Caterpillar? + de: Was kannst du mir über die Caterpillar erzählen? + answer: + en: The Constellation Taurus is a dedicated freighter, designed for hauling cargo. It has a cargo capacity of 174 SCU and is fully configurable but without all the bells and whistles found on other Constellation variants. On the other hand, the Constellation Andromeda is a multi-person freighter and the most popular ship in RSI's current production array. It has a cargo capacity of 96 SCU and is beloved by smugglers and merchants alike for its modular and high-powered capabilities. Both are part of the Constellation series, but the Taurus specifically caters to dedicated freight operations whereas the Andromeda serves as a multi-person versatile ship. + de: Die Constellation Taurus ist ein reiner Frachter, der für den Transport von Fracht entwickelt wurde. Er hat eine Ladekapazität von 174 SCU und ist voll konfigurierbar, hat aber nicht den ganzen Schnickschnack der anderen Constellation-Varianten. Die Constellation Andromeda hingegen ist ein Mehrpersonen-Frachter und das beliebteste Schiff in der aktuellen Produktion von RSI. Sie hat eine Ladekapazität von 96 SCU und ist bei Schmugglern und Händlern wegen ihrer modularen und leistungsstarken Fähigkeiten gleichermaßen beliebt. Beide gehören zur Constellation-Serie, aber die Taurus ist speziell für den reinen Frachtverkehr gedacht, während die Andromeda ein vielseitiges Schiff für mehrere Personen ist. + prompt: | + You have access to the StarHead API which you can use to access live trading data and to retrieve additional information about spaceships in Star Citizen. + Your job is to find good trading routes for the user based on his/her ship, current location and available budget. + The user can also ask you about details of specific ships, components, weapons, and more. + You always use the tools available to you to retrieve the required information and to provide the user with the information. + custom_properties: + - hint: The URL of the StarHead API. + id: starhead_api_url + name: StarHead API URL + required: true + value: https://api.star-head.de + - hint: The URL of the Star Citizen Wiki API. + id: star_citizen_wiki_api_url + name: Star Citizen Wiki API URL + required: true + value: https://api.star-citizen.wiki/api/v2 commands: - name: ToggleCruiseControlOrToggleHoldCurrentSpeed actions: @@ -259,7 +300,7 @@ commands: - keyboard: hotkey: ^ hotkey_codes: - - 41 + - 41 hotkey_extended: false - wait: 0.5 - write: r_DisplayInfo 2 @@ -267,12 +308,12 @@ commands: - keyboard: hotkey: enter hotkey_codes: - - 28 + - 28 hotkey_extended: false - keyboard: hotkey: ^ hotkey_codes: - - 41 + - 41 hotkey_extended: false is_system_command: false instant_activation: @@ -284,7 +325,7 @@ commands: - keyboard: hotkey: ^ hotkey_codes: - - 41 + - 41 hotkey_extended: false - wait: 0.5 - write: r_DisplayInfo 0 @@ -292,12 +333,12 @@ commands: - keyboard: hotkey: enter hotkey_codes: - - 28 + - 28 hotkey_extended: false - keyboard: hotkey: ^ hotkey_codes: - - 41 + - 41 hotkey_extended: false is_system_command: false instant_activation: From 535cc3cceeab6289d1bb798698f30cb056d45a9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Hopst=C3=A4tter?= Date: Tue, 21 May 2024 16:22:34 +0200 Subject: [PATCH 03/10] add endpoint to save basic config --- api/interface.py | 21 ++++++++++++------ services/config_service.py | 44 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 59 insertions(+), 6 deletions(-) diff --git a/api/interface.py b/api/interface.py index 4e936383..07b21571 100644 --- a/api/interface.py +++ b/api/interface.py @@ -503,6 +503,18 @@ class NestedConfig(BaseModel): skills: Optional[list[SkillConfig]] = None +class BasicWingmanConfig(BaseModel): + """All configuration options that can be save in the "Basic" config in the client""" + + name: str + disabled: Optional[bool] = False + record_key: Optional[str] = None + record_key_codes: Optional[list[int]] = None + sound: SoundConfig + voice: str | OpenAiTtsVoice + backstory: Optional[str] = None + + class WingmanConfig(NestedConfig): def __getitem__(self, item): return self.extra_properties.get(item) @@ -522,14 +534,11 @@ def __setitem__(self, key, value): description: str """A short description of this Wingman.""" record_key: Optional[str] = None - """The "push-to-talk" key for this wingman. Keep it pressed while talking! - Modifiers for this key are not supported yet. Don't use the same key for multiple wingmen!""" + """The "push-to-talk" key for this wingman. Keep it pressed while talking! Don't use the same key for multiple wingmen!""" record_key_codes: Optional[list[int]] = None - """The "push-to-talk" key code for this wingman. Keep it pressed while talking! - Modifiers for this key are not supported yet. Don't use the same key for multiple wingmen!""" + """The "push-to-talk" key code for this wingman. Keep it pressed while talking! Don't use the same key for multiple wingmen!""" record_mouse_button: Optional[str] = None - """The "push-to-talk" mouse button for this wingman. Keep it pressed while talking! - Don't use the same button for multiple wingmen!""" + """The "push-to-talk" mouse button for this wingman. Keep it pressed while talking! Don't use the same button for multiple wingmen!""" is_voice_activation_default: Optional[bool] = None """If voice activation is enabled and this is true, the Wingman will listen to your voice by default and without saying its name.""" diff --git a/services/config_service.py b/services/config_service.py index 6a32cc20..9f9d37f4 100644 --- a/services/config_service.py +++ b/services/config_service.py @@ -1,6 +1,8 @@ from typing import Optional from fastapi import APIRouter +from api.enums import OpenAiTtsVoice from api.interface import ( + BasicWingmanConfig, ConfigDirInfo, ConfigWithDirInfo, ConfigsInfo, @@ -118,6 +120,12 @@ def __init__(self, config_manager: ConfigManager): endpoint=self.save_wingman_config, tags=tags, ) + self.router.add_api_route( + methods=["POST"], + path="/config/save-wingman-basic", + endpoint=self.save_basic_wingman_config, + tags=tags, + ) self.router.add_api_route( methods=["GET"], path="/available-skills", @@ -277,6 +285,42 @@ async def save_wingman_config( else: self.printr.toast_error(f"{error_message}") + # POST config/save-wingman-basic + async def save_basic_wingman_config( + self, + config_dir: ConfigDirInfo, + wingman_file: WingmanConfigFileInfo, + basic_config: BasicWingmanConfig, + silent: bool = False, + ): + # get the current config + wingman_config = self.config_manager.load_wingman_config( + config_dir=config_dir, wingman_file=wingman_file + ) + + wingman_config.name = basic_config.name + wingman_config.disabled = basic_config.disabled + wingman_config.record_key = basic_config.record_key + wingman_config.record_key_codes = basic_config.record_key_codes + wingman_config.sound = basic_config.sound + wingman_config.prompts.backstory = basic_config.backstory + try: + wingman_config.openai.tts_voice = OpenAiTtsVoice(basic_config.voice) + except ValueError: + wingman_config.azure.tts.voice = basic_config.voice + + self.config_manager.save_wingman_config( + config_dir=config_dir, + wingman_file=wingman_file, + wingman_config=wingman_config, + ) + try: + if not silent: + await self.load_config(config_dir) + self.printr.toast("Wingman saved successfully.") + except Exception as e: + self.printr.toast_error(f"Invalid Wingman configuration: {str(e)}") + # POST config/wingman/default async def set_default_wingman( self, From d7f6df4f908e6b66011bb91b877f1392dd2d933a Mon Sep 17 00:00:00 2001 From: Jan Winkler <44277746+SawPsyder@users.noreply.github.com> Date: Tue, 21 May 2024 20:50:38 +0200 Subject: [PATCH 04/10] remove alt gr from automated scan code mapping on windows (#155) --- keyboard/keyboard/_winkeyboard.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/keyboard/keyboard/_winkeyboard.py b/keyboard/keyboard/_winkeyboard.py index 63b40eb8..4543650d 100644 --- a/keyboard/keyboard/_winkeyboard.py +++ b/keyboard/keyboard/_winkeyboard.py @@ -377,7 +377,8 @@ def _setup_name_tables(): # Remember the "id" of the name, as the first techniques # have better results and therefore priority. for i, name in enumerate(map(normalize_name, names + lowercase_names)): - from_name[name].append((i, entry)) + if name != "alt gr": # alt gr gets added manually later + from_name[name].append((i, entry)) # TODO: single quotes on US INTL is returning the dead key (?), and therefore # not typing properly. From 89be6ca504d5ce71b62cfbff530cb2fd5d60928c Mon Sep 17 00:00:00 2001 From: Jan Winkler <44277746+SawPsyder@users.noreply.github.com> Date: Tue, 21 May 2024 21:51:10 +0200 Subject: [PATCH 05/10] fix hotkey detection with wingmen without hotkey (#156) --- wingman_core.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wingman_core.py b/wingman_core.py index c133a8f7..d477c03f 100644 --- a/wingman_core.py +++ b/wingman_core.py @@ -186,7 +186,7 @@ def on_press(self, key=None, button=None): wingman = None if key: for potential_wingman in self.tower.wingmen: - if self.is_hotkey_pressed(potential_wingman.get_record_key()): + if potential_wingman.get_record_key() and self.is_hotkey_pressed(potential_wingman.get_record_key()): wingman = potential_wingman elif button: wingman = self.tower.get_wingman_from_mouse(button) From 54cce008deb368e03627c4a6b1edc88eeb3adb38 Mon Sep 17 00:00:00 2001 From: Jan Winkler <44277746+SawPsyder@users.noreply.github.com> Date: Tue, 21 May 2024 21:53:18 +0200 Subject: [PATCH 06/10] Uexcorp skill update (#157) * fixing uexcorp skill for api v2 #part 1 * fixing uexcorp skill for api v2 #part 2 adding actual scu availability option and a more robust data fetching and error logging system * uexcorp skill move loading into its own thread * add uexcorp skill to template --- skills/uexcorp/default_config.yaml | 25 +- skills/uexcorp/main.py | 1066 ++++--- templates/skills/uexcorp/default_config.yaml | 197 ++ templates/skills/uexcorp/logo.png | Bin 0 -> 7506 bytes templates/skills/uexcorp/main.py | 2906 ++++++++++++++++++ 5 files changed, 3684 insertions(+), 510 deletions(-) create mode 100644 templates/skills/uexcorp/default_config.yaml create mode 100644 templates/skills/uexcorp/logo.png create mode 100644 templates/skills/uexcorp/main.py diff --git a/skills/uexcorp/default_config.yaml b/skills/uexcorp/default_config.yaml index 6208e38e..a5a2c7f0 100644 --- a/skills/uexcorp/default_config.yaml +++ b/skills/uexcorp/default_config.yaml @@ -53,6 +53,7 @@ prompt: | You have tools to access the UEXcorp API which you can use to retrieve live trading data and additional information about ships, locations, commodities and more in Star Citizen. Here are some examples when to use the different tools at your disposal: + Do not use markdown formatting (e.g. **name**) in your answers, but prefer lists to show multiple options or information. Do not (never) translate any properties when giving them to the player. They must stay in english or untouched. Only give functions parameters that were previously clearly provided by a request. Never assume any values, not the current ship, not the location, not the available money, nothing! Always send a None-value instead. If you are not using one of the definied functions, dont give any trading recommendations. @@ -108,18 +109,21 @@ custom_properties: - id: uexcorp_api_url name: API URL hint: The URL of the UEX corp API. - value: https://portal.uexcorp.space/api + value: https://uexcorp.space/api/2.0/ required: true property_type: string - id: uexcorp_api_timeout name: API Timeout - hint: The timeout for the UEX corp API in seconds. - value: 5 + hint: The timeout for the UEX corp API in seconds. (If set below 3s, 3s will be used.) + value: 10 + required: true + property_type: number + - id: uexcorp_api_timeout_retries + name: API Timeout Retries + hint: How often the request should be retried in case of a timeout. (Timeout setting may increase automatically on each retry.) + value: 3 required: true property_type: number - # ────────────────────────────── No touchy zone end ────────────────────────────── - - # ──────────────────────── uexcorp specific config start ───────────────────────── # Set this option to "true" to enable caching of the UEX corp API responses. This is recommended, as the API key's quota is very limited. # If you set this option to "false", the Wingman will fetch all data from the UEX corp API on every start. # If you want to update the prices, just tell the Wingman to do so. @@ -182,3 +186,12 @@ custom_properties: value: 1 required: true property_type: number + + # Set this option to true to take estimated scu availability into account for trade route calculations. + # This will reduce the amount of trade routes shown, but will give you more accurate results. + - id: uexcorp_use_estimated_availability + name: Use Estimated Availability + hint: Enable this option to take estimated scu availability into account for trade route calculations. + value: true + required: true + property_type: boolean diff --git a/skills/uexcorp/main.py b/skills/uexcorp/main.py index 8ba262c2..3cb82afd 100644 --- a/skills/uexcorp/main.py +++ b/skills/uexcorp/main.py @@ -1,12 +1,13 @@ """Wingman AI Skill to utalize uexcorp api for trade recommendations""" -import copy +import asyncio +import threading import difflib import heapq import itertools import json -import logging import math +import traceback from os import path import collections import re @@ -23,34 +24,11 @@ from services.file import get_writable_dir from skills.skill_base import Skill - class UEXCorp(Skill): # enable for verbose logging DEV_MODE = False - # TODO: retrieve this from an API - MANUFACTURERS = { - "AEGS": "Aegis Dynamics", - "ANVL": "Anvil Aerospace", - "AOPO": "Aopoa", - "ARGO": "ARGO Astronautics", - "BANU": "Banu", - "CNOU": "Consolidated Outland", - "CRUS": "Crusader Industries", - "DRAK": "Drake Interplanetary", - "ESPR": "Esperia", - "GATA": "Gatac", - "GRIN": "Greycat Industrial", - "KRIG": "Kruger Intergalactic", - "MIRA": "Mirai", - "MISC": "Musashi Industrial & Starflight Concern", - "ORIG": "Origin Jumpworks", - "RSIN": "Roberts Space Industries", - "TMBL": "Tumbril Land Systems", - "VNDL": "Vanduul", - } - def __init__( self, config: SkillConfig, @@ -65,34 +43,42 @@ def __init__( self.logfileerror = path.join(self.data_path, "error.log") self.logfiledebug = path.join(self.data_path, "debug.log") self.cachefile = path.join(self.data_path, "cache.json") - logging.basicConfig(filename=self.logfileerror, level=logging.ERROR) - self.uexcorp_version = "v11" + self.skill_version = "v12" + self.skill_loaded = False + self.skill_loaded_asked = False # init of config options self.uexcorp_api_url: str = None self.uexcorp_api_key: str = None self.uexcorp_api_timeout: int = None + self.uexcorp_api_timeout_retries: int = None self.uexcorp_cache: bool = None self.uexcorp_cache_duration: int = None self.uexcorp_summarize_routes_by_commodity: bool = None self.uexcorp_tradestart_mandatory: bool = None self.uexcorp_trade_blacklist = [] self.uexcorp_default_trade_route_count: int = None + self.uexcorp_use_estimated_availability: bool = None # init of data lists + # self.status_codes = [] + self.ships = [] self.ship_names = [] self.ship_dict = {} self.ship_code_dict = {} + self.commodities = [] self.commodity_names = [] self.commodity_dict = {} self.commodity_code_dict = {} + self.systems = [] self.system_names = [] self.system_dict = {} self.system_code_dict = {} + self.tradeports = [] self.tradeport_names = [] self.tradeport_dict = {} @@ -101,16 +87,19 @@ def __init__( self.tradeports_by_planet = collections.defaultdict(list) self.tradeports_by_satellite = collections.defaultdict(list) self.tradeports_by_city = collections.defaultdict(list) + self.planets = [] self.planet_names = [] self.planet_dict = {} self.planet_code_dict = {} self.planets_by_system = collections.defaultdict(list) + self.satellites = [] self.satellite_names = [] self.satellite_dict = {} self.satellite_code_dict = {} self.satellites_by_planet = collections.defaultdict(list) + self.cities = [] self.city_names = [] self.city_dict = {} @@ -128,7 +117,7 @@ def __init__( self.dynamic_context = "" - async def _print(self, message: str | dict, is_extensive: bool = False) -> None: + async def _print(self, message: str | dict, is_extensive: bool = False, is_debug: bool = True) -> None: """ Prints a message if debug mode is enabled. Will be sent to the server terminal, log file and client. @@ -139,7 +128,7 @@ async def _print(self, message: str | dict, is_extensive: bool = False) -> None: Returns: None """ - if not is_extensive and self.settings.debug_mode: + if (not is_extensive and self.settings.debug_mode) or not is_debug: await self.printr.print_async( message, color=LogType.INFO, @@ -233,7 +222,7 @@ async def validate(self) -> list[WingmanInitializationError]: errors = await super().validate() self.uexcorp_api_key = await self.retrieve_secret( - "uexcorp", errors, "You can get one here: https://uexcorp.space/api.html" + "uexcorp", errors, "You can create your own API key here: https://uexcorp.space/api/apps/" ) self.uexcorp_api_url = self.retrieve_custom_property_value( "uexcorp_api_url", errors @@ -241,6 +230,9 @@ async def validate(self) -> list[WingmanInitializationError]: self.uexcorp_api_timeout = self.retrieve_custom_property_value( "uexcorp_api_timeout", errors ) + self.uexcorp_api_timeout_retries = self.retrieve_custom_property_value( + "uexcorp_api_timeout_retries", errors + ) self.uexcorp_cache = self.retrieve_custom_property_value( "uexcorp_cache", errors ) @@ -258,6 +250,9 @@ async def validate(self) -> list[WingmanInitializationError]: self.uexcorp_default_trade_route_count = self.retrieve_custom_property_value( "uexcorp_default_trade_route_count", errors ) + self.uexcorp_use_estimated_availability = self.retrieve_custom_property_value( + "uexcorp_use_estimated_availability", errors + ) trade_backlist_str: str = self.retrieve_custom_property_value( "uexcorp_trade_blacklist", errors @@ -275,7 +270,7 @@ async def validate(self) -> list[WingmanInitializationError]: ) try: - await self._prepare_data() + await self._start_loading_data() except Exception as e: errors.append( WingmanInitializationError( @@ -287,7 +282,7 @@ async def validate(self) -> list[WingmanInitializationError]: return errors - async def _load_data(self, reload: bool = False) -> None: + async def _load_data(self, reload_prices: bool = False, callback = None) -> None: """ Load data for UEX corp wingman. @@ -295,11 +290,15 @@ async def _load_data(self, reload: bool = False) -> None: reload (bool, optional): Whether to reload the data from the source. Defaults to False. """ - boo_tradeports_reloaded = False + if reload_prices: + await self._load_commodity_prices() + await self._save_to_cachefile() + return + + async def _load_from_cache(): + if not self.uexcorp_cache: + return - save_cache = False - # if cache is enabled and file is not too old, load from cache - if self.uexcorp_cache and not reload: # check file age data = {} try: @@ -313,7 +312,7 @@ async def _load_data(self, reload: bool = False) -> None: data.get("timestamp") and data.get("timestamp") + self.uexcorp_cache_duration > self._get_timestamp() - and data.get("wingman_version") == self.uexcorp_version + and data.get("skill_version") == self.skill_version ): if data.get("ships"): self.ships = data["ships"] @@ -323,77 +322,83 @@ async def _load_data(self, reload: bool = False) -> None: self.systems = data["systems"] if data.get("tradeports"): self.tradeports = data["tradeports"] - boo_tradeports_reloaded = True + # fix prices keys (from string to integer due to unintentional json conversion) + for tradeport in self.tradeports: + if "prices" in tradeport: + tradeport["prices"] = { + int(key): value + for key, value in tradeport["prices"].items() + } if data.get("planets"): self.planets = data["planets"] if data.get("satellites"): self.satellites = data["satellites"] if data.get("cities"): self.cities = data["cities"] - else: - save_cache = True - if not self.ships: - self.ships = await self._fetch_uex_data("ships") - self.ships = [ship for ship in self.ships if ship["implemented"] == "1"] + async def _load_missing_data(): + if not self.ships: + self.ships = await self._fetch_uex_data("vehicles") + self.ships = [ship for ship in self.ships if ship["game_version"]] - if not self.commodities: - self.commodities = await self._fetch_uex_data("commodities") + if not self.commodities: + self.commodities = await self._fetch_uex_data("commodities") + self.commodities = [commodity for commodity in self.commodities if commodity["is_available"] == 1] - if not self.systems: - self.systems = await self._fetch_uex_data("star_systems") - self.systems = [ - system for system in self.systems if system["available"] == 1 - ] - for system in self.systems: - self.tradeports += await self._fetch_uex_data( - f"tradeports/system/{system['code']}" - ) - self.cities += await self._fetch_uex_data( - f"cities/system/{system['code']}" - ) - self.satellites += await self._fetch_uex_data( - f"satellites/system/{system['code']}" - ) - self.planets += await self._fetch_uex_data( - f"planets/system/{system['code']}" + if not self.systems: + self.systems = await self._fetch_uex_data("star_systems") + self.systems = [ + system for system in self.systems if system["is_available"] == 1 + ] + for system in self.systems: + self.tradeports += await self._fetch_uex_data( + f"terminals/id_star_system/{system['id']}/type/commodity/is_available/1/is_visible/1" + ) + self.cities += await self._fetch_uex_data( + f"cities/id_star_system/{system['id']}" + ) + self.satellites += await self._fetch_uex_data( + f"moons/id_star_system/{system['id']}" + ) + self.planets += await self._fetch_uex_data( + f"planets/id_star_system/{system['id']}" ) + await self._load_commodity_prices() - self.tradeports = [ - tradeport - for tradeport in self.tradeports - if tradeport["visible"] == "1" - ] - boo_tradeports_reloaded = True + # data manipulation + planet_codes = [] + for planet in self.planets: + if planet["code"] not in planet_codes: + planet_codes.append(planet["code"]) - self.planets = [ - planet for planet in self.planets if planet["available"] == 1 - ] + for tradeport in self.tradeports: + if ( + tradeport["id_space_station"] + and len(tradeport["nickname"].split("-")) == 2 + and tradeport["nickname"].split("-")[0] in planet_codes + and re.match(r"^L\d+$", tradeport["nickname"].split("-")[1]) + ): + tradeport["id_planet"] = "" - self.satellites = [ - satellite - for satellite in self.satellites - if satellite["available"] == 1 - ] - else: - if reload: - self.tradeports = [] - for system in self.systems: - self.tradeports += await self._fetch_uex_data( - f"tradeports/system/{system['code']}" - ) + def _load_data(callback=None): + async def _actual_loading(callback=None): + await _load_from_cache() + await _load_missing_data() + await self._save_to_cachefile() - self.tradeports = [ - tradeport - for tradeport in self.tradeports - if tradeport["visible"] == "1" - ] - boo_tradeports_reloaded = True - save_cache = True + if callback: + await callback() + new_loop = asyncio.new_event_loop() + asyncio.set_event_loop(new_loop) + new_loop.run_until_complete(_actual_loading(callback)) + new_loop.close() + + threading.Thread(target=_load_data, args=(callback,)).start() + + async def _save_to_cachefile(self) -> None: if ( - save_cache - and self.uexcorp_cache + self.uexcorp_cache and self.uexcorp_cache_duration > 0 and self.ships and self.commodities @@ -405,7 +410,7 @@ async def _load_data(self, reload: bool = False) -> None: ): data = { "timestamp": self._get_timestamp(), - "wingman_version": self.uexcorp_version, + "skill_version": self.skill_version, "ships": self.ships, "commodities": self.commodities, "systems": self.systems, @@ -417,50 +422,69 @@ async def _load_data(self, reload: bool = False) -> None: with open(self.cachefile, "w", encoding="UTF-8") as f: json.dump(data, f, indent=4) - # data manipulation - # remove planet information from space tradeports - if boo_tradeports_reloaded is True: - planet_codes = [] - for planet in self.planets: - if planet["code"] not in planet_codes: - planet_codes.append(planet["code"]) + async def _load_commodity_prices(self) -> None: + """ + Load commodity prices from UEX corp API. - for tradeport in self.tradeports: - shorted_name = tradeport["name"].split(" ")[0] - if ( - tradeport["space"] == "1" - and len(shorted_name.split("-")) == 2 - and shorted_name.split("-")[0] in planet_codes - and re.match(r"^L\d+$", shorted_name.split("-")[1]) - ): - tradeport["planet"] = "" - # remove urls from ships and resolve manufacturer code - for ship in self.ships: - ship.pop("store_url", None) - ship.pop("photos", None) - ship.pop("brochure_url", None) - ship.pop("hotsite_url", None) - ship.pop("video_url", None) - # remove unavailable cities - self.cities = [city for city in self.cities if city["available"] == 1] - # add hull trading option to trade ports - tradeports_for_hull_trading = [ - "Baijini Point", - "Everus Harbor", - "Magnus Gateway", - "Pyro Gateway", - "Seraphim Station", - "Terra Gateway", - "Port Tressler", - ] - for tradeport in self.tradeports: - tradeport["hull_trading"] = tradeport["name"] in tradeports_for_hull_trading - # add hull trading option to ships - ships_for_hull_trading = ["Hull C"] - for ship in self.ships: - ship["hull_trading"] = ship["name"] in ships_for_hull_trading + Returns: + None + """ - async def _prepare_data(self) -> None: + self.cache["readable_objects"] = {} + + # currently the prices are saved in api v1 style to minimize rework time for now + for i in range(0, len(self.tradeports), 10): + tradeports_batch = self.tradeports[i : i + 10] + tradeport_ids = [tradeport["id"] for tradeport in tradeports_batch] + + commodity_prices = await self._fetch_uex_data( + "commodities_prices/id_terminal/" + ",".join(map(str, tradeport_ids)) + ) + + for tradeport in tradeports_batch: + tradeport["prices"] = {} + + for commodity_price in commodity_prices: + if commodity_price["id_terminal"] == tradeport["id"]: + commodity = next( + ( + commodity + for commodity in self.commodities + if commodity["id"] == commodity_price["id_commodity"] + ), + None, + ) + if commodity: + transaction_type = "buy" if commodity_price["price_buy"] > 0 else "sell" + price = { + "name": self._format_commodity_name(commodity), + "kind": commodity["kind"], + "operation": transaction_type, + "price_buy": commodity_price["price_buy"], + "price_sell": commodity_price["price_sell"], + "date_update": commodity_price["date_modified"], + "is_updated": bool(commodity_price["date_modified"]), + "scu": commodity_price[f"scu_{transaction_type}"] or None, + "scu_average": commodity_price[f"scu_{transaction_type}_avg"] or None, + "scu_average_week": commodity_price[f"scu_{transaction_type}_avg_week"] or None, + } + # calculate expected scu + count = 0 + total = 0 + if price["scu"]: + count += 2 + total += (price["scu"] * 2) + if price["scu_average"]: + count += 1 + total += price["scu_average"] + if price["scu_average_week"]: + count += 1 + total += price["scu_average_week"] + price["scu_expected"] = int(total / count) if count > 0 else None + + tradeport["prices"][commodity["id"]] = price + + async def _start_loading_data(self) -> None: """ Prepares the wingman for execution by initializing necessary variables and loading data. @@ -472,18 +496,67 @@ async def _prepare_data(self) -> None: Returns: None """ - # self.start_execution_benchmark() - await self._load_data() + + # fix api url + if self.uexcorp_api_url and self.uexcorp_api_url.endswith("/"): + self.uexcorp_api_url = self.uexcorp_api_url[:-1] + + # fix timeout + self.uexcorp_api_timeout = max(3, self.uexcorp_api_timeout) + self.uexcorp_api_timeout_retries = max(0, self.uexcorp_api_timeout_retries) + + await self._load_data(False, self._prepare_data) + + async def _prepare_data(self) -> None: + """ + Prepares the wingman for execution by initializing necessary variables. + """ + + self.planets = [ + planet for planet in self.planets if planet["is_available"] == 1 + ] + + self.satellites = [ + satellite + for satellite in self.satellites + if satellite["is_available"] == 1 + ] + + # remove urls from ships + for ship in self.ships: + ship.pop("url_store", None) + ship.pop("url_brochure", None) + ship.pop("url_hotsite", None) + ship.pop("url_video", None) + ship.pop("url_photos", None) + + # remove screenshot from tradeports + for tradeport in self.tradeports: + tradeport.pop("screenshot", None) + tradeport.pop("screenshot_thumbnail", None) + tradeport.pop("screenshot_author", None) + + # add hull trading option to trade ports + for tradeport in self.tradeports: + tradeport["hull_trading"] = bool(tradeport["has_loading_dock"]) + + # add hull trading option to ships + ships_for_hull_trading = [ + "Hull C", + "Hull D", + "Hull E", + ] + for ship in self.ships: + ship["hull_trading"] = ship["name"] in ships_for_hull_trading self.ship_names = [ self._format_ship_name(ship) for ship in self.ships - if ship["implemented"] == "1" ] self.ship_dict = { self._format_ship_name(ship).lower(): ship for ship in self.ships } - self.ship_code_dict = {ship["code"].lower(): ship for ship in self.ships} + self.ship_code_dict = {ship["id"]: ship for ship in self.ships} self.commodity_names = [ self._format_commodity_name(commodity) for commodity in self.commodities @@ -493,7 +566,7 @@ async def _prepare_data(self) -> None: for commodity in self.commodities } self.commodity_code_dict = { - commodity["code"].lower(): commodity for commodity in self.commodities + commodity["id"]: commodity for commodity in self.commodities } self.system_names = [ @@ -503,7 +576,7 @@ async def _prepare_data(self) -> None: self._format_system_name(system).lower(): system for system in self.systems } self.system_code_dict = { - system["code"].lower(): system for system in self.systems + system["id"]: system for system in self.systems } self.tradeport_names = [ @@ -514,27 +587,27 @@ async def _prepare_data(self) -> None: for tradeport in self.tradeports } self.tradeport_code_dict = { - tradeport["code"].lower(): tradeport for tradeport in self.tradeports + tradeport["id"]: tradeport for tradeport in self.tradeports } for tradeport in self.tradeports: - if tradeport["system"]: - self.tradeports_by_system[tradeport["system"].lower()].append(tradeport) - if tradeport["planet"]: - self.tradeports_by_planet[tradeport["planet"].lower()].append(tradeport) - if tradeport["satellite"]: - self.tradeports_by_satellite[tradeport["satellite"].lower()].append( + if tradeport["id_star_system"]: + self.tradeports_by_system[tradeport["id_star_system"]].append(tradeport) + if tradeport["id_planet"]: + self.tradeports_by_planet[tradeport["id_planet"]].append(tradeport) + if tradeport["id_moon"]: + self.tradeports_by_satellite[tradeport["id_moon"]].append( tradeport ) - if tradeport["city"]: - self.tradeports_by_city[tradeport["city"].lower()].append(tradeport) + if tradeport["id_city"]: + self.tradeports_by_city[tradeport["id_city"]].append(tradeport) self.city_names = [self._format_city_name(city) for city in self.cities] self.city_dict = { self._format_city_name(city).lower(): city for city in self.cities } - self.city_code_dict = {city["code"].lower(): city for city in self.cities} + self.city_code_dict = {city["id"]: city for city in self.cities} for city in self.cities: - self.cities_by_planet[city["planet"].lower()].append(city) + self.cities_by_planet[city["id_planet"]].append(city) self.satellite_names = [ self._format_satellite_name(satellite) for satellite in self.satellites @@ -544,10 +617,10 @@ async def _prepare_data(self) -> None: for satellite in self.satellites } self.satellite_code_dict = { - satellite["code"].lower(): satellite for satellite in self.satellites + satellite["id"]: satellite for satellite in self.satellites } for satellite in self.satellites: - self.satellites_by_planet[satellite["planet"].lower()].append(satellite) + self.satellites_by_planet[satellite["id_planet"]].append(satellite) self.planet_names = [ self._format_planet_name(planet) for planet in self.planets @@ -556,10 +629,10 @@ async def _prepare_data(self) -> None: self._format_planet_name(planet).lower(): planet for planet in self.planets } self.planet_code_dict = { - planet["code"].lower(): planet for planet in self.planets + planet["id"]: planet for planet in self.planets } for planet in self.planets: - self.planets_by_system[planet["system"].lower()].append(planet) + self.planets_by_system[planet["id_star_system"]].append(planet) self.location_names_set = set( self.system_names @@ -569,6 +642,11 @@ async def _prepare_data(self) -> None: + self.planet_names ) + self.skill_loaded = True + if self.skill_loaded_asked: + self.skill_loaded_asked = False + await self._print("UEXcorp skill data loading complete.", False, False) + def _add_context(self, content: str): """ Adds additional context to the first message content, @@ -600,7 +678,7 @@ def _get_header(self): dict: The header dictionary with the API key. """ key = self.uexcorp_api_key - return {"api_key": key} + return {"Authorization": f"Bearer {key}"} async def _fetch_uex_data( self, endpoint: str, params: Optional[dict[str, any]] = None @@ -616,21 +694,35 @@ async def _fetch_uex_data( list[dict[str, any]]: The fetched data as a list of dictionaries. """ url = f"{self.uexcorp_api_url}/{endpoint}" + await self._print(f"Fetching data from {url} ...", True) - try: - response = requests.get( - url, - params=params, - timeout=self.uexcorp_api_timeout, - headers=self._get_header(), - ) - response.raise_for_status() - except requests.exceptions.RequestException as e: - await self._print(f"Error while retrieving data from {url}: {e}") - return [] + request_count = 1 + timeout_error = False + requests_error = False - # if self.config.debug_mode: - # self.print_execution_time(reset_timer=True) + while request_count == 1 or (request_count <= (self.uexcorp_api_timeout + 1) and timeout_error): + if requests_error: + await self._print(f"Retrying request #{request_count}...", True) + requests_error = False + + timeout_error = False + try: + response = requests.get( + url, + params=params, + timeout=(self.uexcorp_api_timeout * request_count), + headers=self._get_header(), + ) + response.raise_for_status() + except requests.exceptions.RequestException as e: + await self._print(f"Error while retrieving data from {url}: {e}") + requests_error = True + if isinstance(e, requests.exceptions.Timeout): + timeout_error = True + request_count += 1 + + if requests_error: + return [] response_json = response.json() if "status" not in response_json or response_json["status"] != "ok": @@ -651,7 +743,7 @@ def _format_ship_name(self, ship: dict[str, any]) -> str: Returns: str: The formatted ship name. """ - return ship["name"] + return ship["name_full"] def _format_tradeport_name(self, tradeport: dict[str, any]) -> str: """ @@ -663,7 +755,7 @@ def _format_tradeport_name(self, tradeport: dict[str, any]) -> str: Returns: str: The formatted tradeport name. """ - return tradeport["name"] + return tradeport["nickname"] def _format_city_name(self, city: dict[str, any]) -> str: """ @@ -964,15 +1056,23 @@ async def execute_tool( try: if tool_name in functions: + if(not self.skill_loaded): + self.skill_loaded_asked = True + await self._print("UEXcorp skill is not loaded yet. Please wait a moment.", False, False) + function_response = "Data is still beeing loaded. Please wait a moment." + return function_response, instant_response + self.start_execution_benchmark() await self._print(f"Executing function: {tool_name}") function = getattr(self, "_gpt_call_" + functions[tool_name]) function_response = await function(**parameters) if self.settings.debug_mode: await self.print_execution_time() - except Exception as e: - logging.error(e, exc_info=True) + if self.DEV_MODE: + await self._print(f"_gpt_call_{functions[tool_name]} response: {function_response}", True) + except Exception: file_object = open(self.logfileerror, "a", encoding="UTF-8") + file_object.write(traceback.format_exc()) file_object.write( "========================================================================================\n" ) @@ -981,7 +1081,7 @@ async def execute_tool( ) file_object.write(f"With parameters: {parameters}\n") file_object.write(f"On date: {datetime.now()}\n") - file_object.write(f"Version: {self.uexcorp_version}\n") + file_object.write(f"Version: {self.skill_version}\n") file_object.write( "========================================================================================\n" ) @@ -990,7 +1090,7 @@ async def execute_tool( f"Error while executing custom function: {tool_name}\nCheck log file for more details." ) function_response = f"Error while executing custom function: {tool_name}" - function_response += "\nTell user there seems to be an error. And you must say that it should be report to the 'uexcorp wingman developers'." + function_response += "\nTell user there seems to be an error. And you must say that it should be report to the 'uexcorp skill developer (JayMatthew on Discord)'." return function_response, instant_response @@ -1118,7 +1218,7 @@ async def _gpt_call_reload_current_commodity_prices(self) -> str: Returns: str: A message indicating that the current commodity prices have been reloaded. """ - await self._load_data(reload=True) + await self._load_data(True) # clear cached data for key in self.cache: self.cache[key] = {} @@ -1257,7 +1357,7 @@ async def _gpt_call_get_ship_comparison(self, ship_names: list[str] = None) -> s ) output = ( - "Point out differences between these ships but keep it short, like 4-5 sentences, and dont mention something both cant do, like getting rented:\n" + "Point out differences between these ships but keep it short, like 4-5 sentences, and dont mention something both cant do:\n" + json.dumps(output) ) self._log(output, True) @@ -1343,62 +1443,46 @@ def _get_converted_tradeport_for_output( Returns: dict[str, any]: The converted tradeport dictionary. """ - checksum = f"tradeport--{tradeport['code']}" + checksum = f"tradeport--{tradeport['id']}" if checksum in self.cache["readable_objects"]: return self.cache["readable_objects"][checksum] - tradeport = copy.deepcopy(tradeport) - deletable_keys = [ - "code", - "name_short", - "space", - "visible", - "prices", - "date_modified", - "date_added", - ] - tradeport["type"] = "Tradeport" - tradeport["system"] = self._get_system_name_by_code(tradeport["system"]) - tradeport["planet"] = self._get_planet_name_by_code(tradeport["planet"]) - tradeport["city"] = self._get_city_name_by_code(tradeport["city"]) - tradeport["satellite"] = self._get_satellite_name_by_code( - tradeport["satellite"] - ) - tradeport["hull_trading"] = ( - "Trading with MISC Hull C is possible." + output = { + "type": "Tradeport", + "name": self._format_tradeport_name(tradeport), + "star_system": self._get_system_name_by_code(tradeport["id_star_system"]), + "planet": self._get_planet_name_by_code(tradeport["id_planet"]), + "city": self._get_city_name_by_code(tradeport["id_city"]), + "satellite": self._get_satellite_name_by_code(tradeport["id_moon"]), + } + output["hull_trading"] = ( + "Trading with large ships, that need a loading area, is possible." if tradeport["hull_trading"] - else "Trading with MISC Hull C is not possible." + else "Trading with large ships, that need a loading area, is not possible." ) - if "prices" in tradeport: - buyable_commodities = [ - f"{data['name']} for {data['price_buy']} aUEC per SCU" - for commodity_code, data in tradeport["prices"].items() - if data["operation"] == "buy" - ] - sellable_commodities = [ - f"{data['name']} for {data['price_sell']} aUEC per SCU" - for commodity_code, data in tradeport["prices"].items() - if data["operation"] == "sell" - ] - else: - buyable_commodities = [] - sellable_commodities = [] + buyable_commodities = [ + f"{data['name']} for {data['price_buy']} aUEC per SCU" + for commodity_code, data in tradeport["prices"].items() + if data["operation"] == "buy" + ] + sellable_commodities = [ + f"{data['name']} for {data['price_sell']} aUEC per SCU" + for commodity_code, data in tradeport["prices"].items() + if data["operation"] == "sell" + ] if len(buyable_commodities): - tradeport["buyable_commodities"] = ", ".join(buyable_commodities) + output["buyable_commodities"] = ", ".join(buyable_commodities) if len(sellable_commodities): - tradeport["sellable_commodities"] = ", ".join(sellable_commodities) + output["sellable_commodities"] = ", ".join(sellable_commodities) for key in ["system", "planet", "city", "satellite"]: - if tradeport.get(key) is None: - deletable_keys.append(key) - - for key in deletable_keys: - tradeport.pop(key, None) + if output.get(key) is None: + output.pop(key, None) - self.cache["readable_objects"][checksum] = tradeport - return tradeport + self.cache["readable_objects"][checksum] = output + return output def _get_converted_city_for_output(self, city: dict[str, any]) -> dict[str, any]: """ @@ -1410,32 +1494,38 @@ def _get_converted_city_for_output(self, city: dict[str, any]) -> dict[str, any] Returns: dict[str, any]: The converted city dictionary. """ - checksum = f"city--{city['code']}" + checksum = f"city--{city['id']}" if checksum in self.cache["readable_objects"]: return self.cache["readable_objects"][checksum] - city = copy.deepcopy(city) - deletable_keys = [ - "code", - "available", - "date_added", - "date_modified", - ] - city["type"] = "City" - city["system"] = self._get_system_name_by_code(city["system"]) - city["planet"] = self._get_planet_name_by_code(city["planet"]) - tradeports = self._get_tradeports_by_position_name(city["name"], True) + output = { + "type": "City", + "name": self._format_city_name(city), + "star_system": self._get_system_name_by_code(city["id_star_system"]), + "planet": self._get_planet_name_by_code(city["id_planet"]), + "moon": self._get_satellite_name_by_code(city["id_moon"]), + "is_armistice": "Yes" if city["is_armistice"] else "No", + "has_freight_elevator": "Yes" if city["has_freight_elevator"] else "No", + "has_docking_ports": "Yes" if city["has_docking_port"] else "No", + "has_clinic": "Yes" if city["has_clinic"] else "No", + "has_food": "Yes" if city["has_food"] else "No", + "has_refuel_option": "Yes" if city["has_refuel"] else "No", + "has_repair_option": "Yes" if city["has_repair"] else "No", + "has_refinery": "Yes" if city["has_refinery"] else "No", + } + tradeports = self._get_tradeports_by_position_name(city["name"]) if tradeports: - city["options_to_trade"] = ", ".join( + output["options_to_trade"] = ", ".join( [self._format_tradeport_name(tradeport) for tradeport in tradeports] ) - for key in deletable_keys: - city.pop(key, None) + for key in ["star_system", "planet", "moon"]: + if output.get(key) is None: + output.pop(key, None) - self.cache["readable_objects"][checksum] = city - return city + self.cache["readable_objects"][checksum] = output + return output def _get_converted_satellite_for_output( self, satellite: dict[str, any] @@ -1449,32 +1539,29 @@ def _get_converted_satellite_for_output( Returns: dict[str, any]: The converted satellite dictionary. """ - checksum = f"satellite--{satellite['code']}" + checksum = f"satellite--{satellite['id']}" if checksum in self.cache["readable_objects"]: return self.cache["readable_objects"][checksum] - satellite = copy.deepcopy(satellite) - deletable_keys = [ - "code", - "available", - "date_added", - "date_modified", - ] - satellite["type"] = "Satellite" - satellite["system"] = self._get_system_name_by_code(satellite["system"]) - satellite["planet"] = self._get_planet_name_by_code(satellite["planet"]) - tradeports = self._get_tradeports_by_position_name(satellite["name"], True) + output = { + "type": "Moon", + "name": self._format_satellite_name(satellite), + "star_system": self._get_system_name_by_code(satellite["id_star_system"]), + "orbits_planet": self._get_planet_name_by_code(satellite["id_planet"]), + } + tradeports = self._get_tradeports_by_position_name(self._format_satellite_name(satellite)) if tradeports: - satellite["options_to_trade"] = ", ".join( + output["options_to_trade"] = ", ".join( [self._format_tradeport_name(tradeport) for tradeport in tradeports] ) - for key in deletable_keys: - satellite.pop(key, None) + for key in ["star_system", "orbits_planet"]: + if output.get(key) is None: + output.pop(key, None) - self.cache["readable_objects"][checksum] = satellite - return satellite + self.cache["readable_objects"][checksum] = output + return output def _get_converted_planet_for_output( self, planet: dict[str, any] @@ -1488,43 +1575,40 @@ def _get_converted_planet_for_output( Returns: dict[str, any]: The converted planet dictionary. """ - checksum = f"planet--{planet['code']}" + checksum = f"planet--{planet['id']}" if checksum in self.cache["readable_objects"]: return self.cache["readable_objects"][checksum] - planet = copy.deepcopy(planet) - deletable_keys = [ - "code", - "available", - "date_added", - "date_modified", - ] - planet["type"] = "Planet" - planet["system"] = self._get_system_name_by_code(planet["system"]) - tradeports = self._get_tradeports_by_position_name(planet["name"], True) + output = { + "type": "Planet", + "name": self._format_planet_name(planet), + "star_system": self._get_system_name_by_code(planet["id_star_system"]), + } + tradeports = self._get_tradeports_by_position_name(planet["name"]) if tradeports: - planet["options_to_trade"] = ", ".join( + output["options_to_trade"] = ", ".join( [self._format_tradeport_name(tradeport) for tradeport in tradeports] ) satellites = self._get_satellites_by_planetcode(planet["code"]) if satellites: - planet["satellites"] = ", ".join( + output["satellites"] = ", ".join( [self._format_satellite_name(satellite) for satellite in satellites] ) cities = self._get_cities_by_planetcode(planet["code"]) if cities: - planet["cities"] = ", ".join( + output["cities"] = ", ".join( [self._format_city_name(city) for city in cities] ) - for key in deletable_keys: - planet.pop(key, None) + for key in ["star_system"]: + if output.get(key) is None: + output.pop(key, None) - self.cache["readable_objects"][checksum] = planet - return planet + self.cache["readable_objects"][checksum] = output + return output def _get_converted_system_for_output( self, system: dict[str, any] @@ -1538,37 +1622,43 @@ def _get_converted_system_for_output( Returns: dict[str, any]: The converted system dictionary. """ - checksum = f"system--{system['code']}" + checksum = f"system--{system['id']}" if checksum in self.cache["readable_objects"]: return self.cache["readable_objects"][checksum] - system = copy.deepcopy(system) - deletable_keys = [ - "code", - "available", - "default", - "date_added", - "date_modified", - ] - system["type"] = "System" - tradeports = self._get_tradeports_by_position_name(system["name"], True) + output = { + "type": "Star System", + "name": self._format_system_name(system), + } + tradeports = self._get_tradeports_by_position_name(system["name"]) if tradeports: - system["options_to_trade"] = ", ".join( - [self._format_tradeport_name(tradeport) for tradeport in tradeports] - ) + output["options_to_trade"] = f"{len(tradeports)} different options to trade." + tradeport_without_planets = [] + gateways = [] + for tradeport in tradeports: + if not tradeport["id_planet"]: + if tradeport["name"].find("Gateway") != -1: + gateways.append(tradeport) + else: + tradeport_without_planets.append(tradeport) + if tradeport_without_planets: + output["space_stations"] = ", ".join( + [self._format_tradeport_name(tradeport) for tradeport in tradeport_without_planets] + ) + if gateways: + output["gateways"] = ", ".join( + [self._format_tradeport_name(tradeport) for tradeport in gateways] + ) planets = self._get_planets_by_systemcode(system["code"]) if planets: - system["planets"] = ", ".join( + output["planets"] = ", ".join( [self._format_planet_name(planet) for planet in planets] ) - for key in deletable_keys: - system.pop(key, None) - - self.cache["readable_objects"][checksum] = system - return system + self.cache["readable_objects"][checksum] = output + return output def _get_converted_ship_for_output(self, ship: dict[str, any]) -> dict[str, any]: """ @@ -1580,120 +1670,112 @@ def _get_converted_ship_for_output(self, ship: dict[str, any]) -> dict[str, any] Returns: dict[str, any]: The converted ship dictionary. """ - checksum = f"ship--{ship['code']}" + checksum = f"ship--{ship['id']}" if checksum in self.cache["readable_objects"]: return self.cache["readable_objects"][checksum] - ship = copy.deepcopy(ship) - deletable_keys = [ - "code", - "loaner", - "scu", - "implemented", - "mining", - "stealth", - "price", - "price_warbond", - "price_pkg", - "sell_active", - "sell_active_warbond", - "sell_active_pkg", - "stock_qt_speed", - "showdown_winner", - "date_added", - "date_modified", - "hull_trading", - ] - ship["type"] = "Ship" - ship["manufacturer"] = ( - self.MANUFACTURERS[ship["manufacturer"]] - if ship["manufacturer"] in self.MANUFACTURERS - else ship["manufacturer"] - ) - ship["cargo"] = f"{ship['scu']} SCU" - ship["price_USD"] = f"{ship['price']}" - - ship["buy_at"] = ( - "This ship cannot be bought." - if not ship["buy_at"] - else [ - self._get_converted_rent_and_buy_option_for_output(position) - for position in ship["buy_at"] - ] - ) + output = { + "type": "Ship" if ship["is_spaceship"] else "Groud Vehicle", + "name": self._format_ship_name(ship), + "manufacturer": ship["company_name"], + "cargo_capacity": f"{ship['scu']} SCU", + "added_on_version": "Unknown" if ship["is_concept"] else ship["game_version"], + "field_of_activity": self._get_ship_field_of_activity(ship), + } - ship["rent_at"] = ( - "This ship cannot be rented." - if not ship["rent_at"] - else [ - self._get_converted_rent_and_buy_option_for_output(position) - for position in ship["rent_at"] - ] - ) + buy_rent_options = self._get_converted_rent_and_buy_option_for_output(ship) + if "buy_at" in buy_rent_options: + output["buy_at"] = buy_rent_options["buy_at"] + + if "rent_at" in buy_rent_options: + output["rent_at"] = buy_rent_options["rent_at"] if ship["hull_trading"] is True: - ship["trading_info"] = ( - "This ship can only trade on suitable space stations with a cargo deck." + output["trading_info"] = ( + "This ship can only trade on suitable space stations with cargo loading option." ) - for key in deletable_keys: - ship.pop(key, None) + self.cache["readable_objects"][checksum] = output + return output + + def _get_ship_field_of_activity(self, ship: dict[str, any]) -> str: + """ + Returns the field of activity of a ship. - self.cache["readable_objects"][checksum] = ship - return ship + Args: + ship (dict[str, any]): The ship dictionary to get the field of activity for. + + Returns: + str: The field of activity of the ship. + """ + + field = [] + if ship["is_exploration"]: + field.append("Exploration") + if ship["is_mining"]: + field.append("Mining") + if ship["is_salvage"]: + field.append("Salvage") + if ship["is_refinery"]: + field.append("Refinery") + if ship["is_scanning"]: + field.append("Scanning") + if ship["is_cargo"]: + field.append("Cargo") + if ship["is_medical"]: + field.append("Medical") + if ship["is_racing"]: + field.append("Racing") + if ship["is_repair"]: + field.append("Repair") + if ship["is_refuel"]: + field.append("Refuel") + if ship["is_interdiction"]: + field.append("Interdiction") + if ship["is_tractor_beam"]: + field.append("Tractor Beam") + if ship["is_qed"]: + field.append("Quantum Interdiction") + if ship["is_emp"]: + field.append("EMP") + if ship["is_construction"]: + field.append("Construction") + if ship["is_datarunner"]: + field.append("Datarunner") + if ship["is_science"]: + field.append("Science") + if ship["is_boarding"]: + field.append("Boarding") + if ship["is_stealth"]: + field.append("Stealth") + if ship["is_research"]: + field.append("Research") + if ship["is_carrier"]: + field.append("Carrier") + + addition = [] + if ship["is_civilian"]: + addition.append("Civilian") + if ship["is_military"]: + addition.append("Military") + + return f"{', '.join(field)} ({' & '.join(addition)})" def _get_converted_rent_and_buy_option_for_output( - self, position: dict[str, any] + self, ship: dict[str, any] ) -> dict[str, any]: """ - Converts a rent/buy option dictionary to a dictionary that can be used as output. + Converts the rent and buy options of a ship to a dictionary that can be used as output. Args: - position (dict[str, any]): The rent/buy option dictionary to be converted. + ship (dict[str, any]): The ship dictionary to get the rent and buy options for. Returns: - dict[str, any]: The converted rent/buy option dictionary. - """ - position = copy.deepcopy(position) - keys = ["system", "planet", "satellite", "city", "store"] - if not position["tradeport"]: - for key in keys: - if not position[key]: - position.pop(key, None) - else: - position[key] = position[f"{key}_name"] - position.pop("tradeport", None) - else: - tradeport = self._get_tradeport_by_code(position["tradeport"]) - position["tradeport"] = self._format_tradeport_name(tradeport) - for key in keys: - function_name = f"_get_{key}_name_by_code" - if function_name in dir(self): - name = getattr(self, function_name)(tradeport[key]) - if name: - position[key] = name - else: - position.pop(key, None) - position["store"] = ( - "Refinery" # TODO: remove this when refinery is implemented - ) - position["price"] = f"{position['price']} aUEC" - - keys_to_remove = [ - "tradeport_name", - "system_name", - "planet_name", - "satellite_name", - "tradeport_name", - "city_name", - "store_name", - "date_added", - "date_modified", - ] - for key in keys_to_remove: - position.pop(key, None) + dict[str, any]: The converted rent and buy options dictionary. + """ - return position + # TODO: implement this with API v2 + return {} def _get_converted_commodity_for_output( self, commodity: dict[str, any] @@ -1707,69 +1789,56 @@ def _get_converted_commodity_for_output( Returns: dict[str, any]: The converted commodity dictionary. """ - checksum = f"commodity--{commodity['code']}" + checksum = f"commodity--{commodity['id']}" if checksum in self.cache["readable_objects"]: return self.cache["readable_objects"][checksum] - commodity = copy.deepcopy(commodity) - commodity["notes"] = "" - deletable_keys = [ - "code", - "tradable", - "temporary", - "restricted", - "raw", - "available", - "date_added", - "date_modified", - "trade_price_buy", - "trade_price_sell", - ] + output = { + "type": "Commodity", + "subtype": commodity["kind"], + "name": commodity["name"], + } price_buy_best = None price_sell_best = None - commodity["buy_options"] = {} - commodity["sell_options"] = {} + output["buy_at"] = {} + output["sell_at"] = {} for tradeport in self.tradeports: if "prices" not in tradeport: continue - if commodity["code"] in tradeport["prices"]: - if tradeport["prices"][commodity["code"]]["operation"] == "buy": - price_buy = tradeport["prices"][commodity["code"]]["price_buy"] + if commodity["id"] in tradeport["prices"]: + if tradeport["prices"][commodity["id"]]["operation"] == "buy": + price_buy = tradeport["prices"][commodity["id"]]["price_buy"] if price_buy_best is None or price_buy < price_buy_best: price_buy_best = price_buy - commodity["buy_options"][ + output["buy_at"][ self._format_tradeport_name(tradeport) ] = f"{price_buy} aUEC" else: - price_sell = tradeport["prices"][commodity["code"]]["price_sell"] + price_sell = tradeport["prices"][commodity["id"]]["price_sell"] if price_sell_best is None or price_sell > price_sell_best: price_sell_best = price_sell - commodity["sell_options"][ + output["sell_at"][ self._format_tradeport_name(tradeport) ] = f"{price_sell} aUEC" - commodity["best_buy_price"] = ( + output["best_buy_price"] = ( f"{price_buy_best} aUEC" if price_buy_best else "Not buyable." ) - commodity["best_sell_price"] = ( + output["best_sell_price"] = ( f"{price_sell_best} aUEC" if price_sell_best else "Not sellable." ) - boolean_keys = ["minable", "harvestable", "illegal"] + boolean_keys = ["is_harvestable", "is_mineral", "is_illegal"] for key in boolean_keys: - commodity[key] = "Yes" if commodity[key] != "0" else "No" - if commodity["illegal"] == "Yes": - commodity[ - "notes" - ] += "Stay away from ship scanns to avoid fines and crimestat, as this commodity is illegal." - - for key in deletable_keys: - commodity.pop(key, None) + output[key] = "Yes" if commodity[key] else "No" - self.cache["readable_objects"][checksum] = commodity - return commodity + if commodity["is_illegal"]: + output["notes"] = "Stay away from ship scanns to avoid fines and crimestat, as this commodity is illegal." + + self.cache["readable_objects"][checksum] = output + return output async def _gpt_call_get_locations_to_sell_to( self, @@ -1860,6 +1929,7 @@ async def _gpt_call_get_locations_to_sell_to( self._get_tradeport_route_description(tradeport) for tradeport in tradeports ) + messages.append("\n") self._log("\n".join(messages), True) return "\n".join(messages) @@ -1952,6 +2022,7 @@ async def _gpt_call_get_locations_to_buy_from( self._get_tradeport_route_description(tradeport) for tradeport in tradeports ) + messages.append("\n") self._log("\n".join(messages), True) return "\n".join(messages) @@ -1967,7 +2038,7 @@ def _get_data_location_sellprice(self, tradeport, commodity, ship=None, amount=1 if "prices" not in tradeport: return None - commodity_code = commodity["code"] + commodity_code = commodity["id"] for code, price in tradeport["prices"].items(): if code == commodity_code and price["operation"] == "sell": return price["price_sell"] * amount @@ -1984,7 +2055,7 @@ def _get_data_location_buyprice(self, tradeport, commodity, ship=None, amount=1) if "prices" not in tradeport: return None - commodity_code = commodity["code"] + commodity_code = commodity["id"] for code, price in tradeport["prices"].items(): if code == commodity_code and price["operation"] == "buy": return price["price_buy"] * amount @@ -2092,8 +2163,6 @@ async def _gpt_call_get_trading_routes( answer += ( f"These given parameters were misunderstood: {misunderstood_str}" ) - - self._log(answer, True) return answer # set variables @@ -2133,21 +2202,21 @@ async def _gpt_call_get_trading_routes( errors = [] for commodity in commodities: commodity_routes = [] - if not illegal_commodities_allowed and commodity["illegal"] == "1": + if not illegal_commodities_allowed and commodity["is_illegal"]: continue for start_tradeport in start_tradeports: if ( "prices" not in start_tradeport - or commodity["code"] not in start_tradeport["prices"] - or start_tradeport["prices"][commodity["code"]]["operation"] + or commodity["id"] not in start_tradeport["prices"] + or start_tradeport["prices"][commodity["id"]]["operation"] != "buy" ): continue for end_tradeport in end_tradeports: if ( "prices" not in end_tradeport - or commodity["code"] not in end_tradeport["prices"] - or end_tradeport["prices"][commodity["code"]]["operation"] + or commodity["id"] not in end_tradeport["prices"] + or end_tradeport["prices"][commodity["id"]]["operation"] != "sell" ): continue @@ -2167,9 +2236,9 @@ async def _gpt_call_get_trading_routes( trading_route_new = self._get_trading_route( ship, start_tradeport, + end_tradeport, money, free_cargo_space, - end_tradeport, commodity, illegal_commodities_allowed, ) @@ -2228,28 +2297,25 @@ async def _gpt_call_get_trading_routes( trading_route["cargo"] = f"{trading_route['cargo']} SCU" message = ( - "Possible commodities with their profit. Just give basic overview at first." + "Possible commodities with their profit. Just give basic overview at first.\n" + additional_answer - + " JSON: " + + " JSON: \n " + json.dumps(trading_routes) ) - - self._log(message, True) return message else: return_string = "No trading routes found." if len(errors) > 0: return_string += "\nPossible errors are:\n- " + "\n- ".join(errors) - await self._print(return_string, True) return return_string def _get_trading_route( self, ship: dict[str, any], position_start: dict[str, any], + position_end: dict[str, any], money: int = None, free_cargo_space: int = None, - position_end: dict[str, any] = None, commodity: dict[str, any] = None, illegal_commodities_allowed: bool = True, ) -> str: @@ -2280,7 +2346,7 @@ def _get_trading_route( return "Your ship has no cargo space to trade." commodity_filter = commodity - start_tradeports = self._get_tradeports_by_position_name(position_start["name"]) + start_tradeports = [position_start] if ship["hull_trading"] is True: start_tradeports = [ tradeport @@ -2292,13 +2358,7 @@ def _get_trading_route( return "No valid start position given. Make sure to provide a start point compatible with your ship." return "No valid start position given. Try a different position or just name a planet or star system." - end_tradeports = [] - if position_end is None: - for tradeport in self.tradeports: - if tradeport["system"] == start_tradeports[0]["system"]: - end_tradeports.append(tradeport) - else: - end_tradeports = self._get_tradeports_by_position_name(position_end["name"]) + end_tradeports = [position_end] if ship["hull_trading"] is True: end_tradeports = [ tradeport @@ -2311,7 +2371,7 @@ def _get_trading_route( if ( len(end_tradeports) == 1 and len(start_tradeports) == 1 - and end_tradeports[0]["code"] == start_tradeports[0]["code"] + and end_tradeports[0]["id"] == start_tradeports[0]["id"] ): return "Start and end position are the same." @@ -2333,7 +2393,7 @@ def _get_trading_route( for blacklist_item in self.uexcorp_trade_blacklist: if "tradeport" in blacklist_item and blacklist_item["tradeport"]: for tradeport in start_tradeports: - if tradeport["name"] == blacklist_item["tradeport"]: + if self._format_tradeport_name(tradeport) == blacklist_item["tradeport"]: if ( "commodity" not in blacklist_item or not blacklist_item["commodity"] @@ -2346,12 +2406,12 @@ def _get_trading_route( blacklist_item["commodity"] ) for commodity_code, data in tradeport["prices"].items(): - if commodity["code"] == commodity_code: + if commodity["id"] == commodity_code: # remove commodity code from tradeport tradeport["prices"].pop(commodity_code) break for tradeport in end_tradeports: - if tradeport["name"] == blacklist_item["tradeport"]: + if self._format_tradeport_name(tradeport) == blacklist_item["tradeport"]: if ( "commodity" not in blacklist_item or not blacklist_item["commodity"] @@ -2364,26 +2424,30 @@ def _get_trading_route( blacklist_item["commodity"] ) for commodity_code, data in tradeport["prices"].items(): - if commodity["code"] == commodity_code: + if commodity["id"] == commodity_code: # remove commodity code from tradeport tradeport["prices"].pop(commodity_code) break + if len(start_tradeports) < 1 or len(end_tradeports) < 1: + return "Exluded by blacklist." + for tradeport_start in start_tradeports: commodities = [] if "prices" not in tradeport_start: continue - for attr, price in tradeport_start["prices"].items(): + for commodity_code, price in tradeport_start["prices"].items(): if price["operation"] == "buy" and ( - commodity_filter is None or commodity_filter["code"] == attr + commodity_filter is None or commodity_filter["id"] == commodity_code ): - commodity = self._get_commodity_by_code(attr) + commodity = self._get_commodity_by_code(commodity_code) if ( illegal_commodities_allowed is True - or commodity["illegal"] != "1" + or not commodity["is_illegal"] ): - price["short_name"] = attr + temp_price = price + temp_price["commodity_code"] = commodity_code in_blacklist = False # apply commodity blacklist @@ -2407,15 +2471,12 @@ def _get_trading_route( continue for tradeport_end in end_tradeports: - if "prices" not in tradeport_end or ( - commodity_filter is not None - and commodity_filter["code"] not in tradeport_end["prices"] - ): + if "prices" not in tradeport_end: continue - for attr, price in tradeport_end["prices"].items(): - price["short_name"] = attr - sell_commodity = self._get_commodity_by_code(attr) + for commodity_code, price in tradeport_end["prices"].items(): + sell_commodity = self._get_commodity_by_code(commodity_code) + in_blacklist = False # apply commodity blacklist if sell_commodity and self.uexcorp_trade_blacklist: @@ -2437,9 +2498,12 @@ def _get_trading_route( if in_blacklist: continue + temp_price = price + temp_price["commodity_code"] = commodity_code + for commodity in commodities: if ( - commodity["short_name"] == price["short_name"] + commodity["commodity_code"] == temp_price["commodity_code"] and price["operation"] == "sell" and price["price_sell"] > commodity["price_buy"] ): @@ -2450,7 +2514,12 @@ def _get_trading_route( money / commodity["price_buy"] ) cargo_by_space = cargo_space - cargo = min(cargo_by_money, cargo_by_space) + if self.uexcorp_use_estimated_availability: + cargo_by_availability = min(commodity["scu_expected"] or 0, temp_price["scu_expected"] or 0) + else: + cargo_by_availability = cargo_by_space + + cargo = min(cargo_by_money, cargo_by_space, cargo_by_availability) if cargo >= 1: profit = round( cargo @@ -2459,7 +2528,7 @@ def _get_trading_route( if profit > best_route["profit"]: best_route["start"] = [tradeport_start] best_route["end"] = [tradeport_end] - best_route["commodity"] = price + best_route["commodity"] = temp_price best_route["profit"] = profit best_route["cargo"] = cargo best_route["buy"] = commodity["price_buy"] * cargo @@ -2467,8 +2536,8 @@ def _get_trading_route( else: if ( profit == best_route["profit"] - and best_route["commodity"]["short_name"] - == price["short_name"] + and best_route["commodity"]["commodity_code"] + == temp_price["commodity_code"] ): if tradeport_start not in best_route["start"]: best_route["start"].append(tradeport_start) @@ -2517,7 +2586,7 @@ def _get_tradeport_by_code(self, code: str) -> dict[str, any] | None: Returns: Optional[object]: The tradeport object if found, otherwise None. """ - return self.tradeport_code_dict.get(code.lower()) if code else None + return self.tradeport_code_dict.get(code) if code else None def _get_planet_by_name(self, name: str) -> dict[str, any] | None: """Finds the planet with the specified name and returns the planet or None. @@ -2585,7 +2654,7 @@ def _get_tradeport_route_description(self, tradeport: dict[str, any]) -> str: """ tradeport = self._get_converted_tradeport_for_output(tradeport) keys = [ - ("system", "Star-System"), + ("star_system", "Star-System"), ("planet", "Planet"), ("satellite", "Satellite"), ("city", "City"), @@ -2604,7 +2673,7 @@ def _get_system_name_by_code(self, code: str) -> str: str: The name of the system with the specified code. """ return ( - self._format_system_name(self.system_code_dict.get(code.lower())) + self._format_system_name(self.system_code_dict.get(code)) if code else None ) @@ -2619,7 +2688,7 @@ def _get_planet_name_by_code(self, code: str) -> str: str: The name of the planet with the specified code. """ return ( - self._format_planet_name(self.planet_code_dict.get(code.lower())) + self._format_planet_name(self.planet_code_dict.get(code)) if code else None ) @@ -2634,7 +2703,7 @@ def _get_satellite_name_by_code(self, code: str) -> str: str: The name of the satellite with the specified code. """ return ( - self._format_satellite_name(self.satellite_code_dict.get(code.lower())) + self._format_satellite_name(self.satellite_code_dict.get(code)) if code else None ) @@ -2649,7 +2718,7 @@ def _get_city_name_by_code(self, code: str) -> str: str: The name of the city with the specified code. """ return ( - self._format_city_name(self.city_code_dict.get(code.lower())) + self._format_city_name(self.city_code_dict.get(code)) if code else None ) @@ -2664,7 +2733,7 @@ def _get_commodity_name_by_code(self, code: str) -> str: str: The name of the commodity with the specified code. """ return ( - self._format_commodity_name(self.commodity_code_dict.get(code.lower())) + self._format_commodity_name(self.commodity_code_dict.get(code)) if code else None ) @@ -2678,10 +2747,10 @@ def _get_commodity_by_code(self, code: str) -> dict[str, any] | None: Returns: Optional[object]: The commodity object if found, otherwise None. """ - return self.commodity_code_dict.get(code.lower()) if code else None + return self.commodity_code_dict.get(code) if code else None def _get_tradeports_by_position_name( - self, name: str, direct: bool = False + self, name: str ) -> list[dict[str, any]]: """Returns all tradeports with the specified position name. @@ -2715,7 +2784,7 @@ def _get_satellites_by_planetcode(self, code: str) -> list[dict[str, any]]: Returns: Optional[object]: The satellite object if found, otherwise None. """ - return self.satellites_by_planet.get(code.lower(), []) if code else [] + return self.satellites_by_planet.get(code, []) if code else [] def _get_cities_by_planetcode(self, code: str) -> list[dict[str, any]]: """Returns all cities with the specified planet code. @@ -2726,7 +2795,7 @@ def _get_cities_by_planetcode(self, code: str) -> list[dict[str, any]]: Returns: list[dict[str, any]]: A list of cities matching the planet code. """ - return self.cities_by_planet.get(code.lower(), []) if code else [] + return self.cities_by_planet.get(code, []) if code else [] def _get_planets_by_systemcode(self, code: str) -> list[dict[str, any]]: """Returns all planets with the specified system code. @@ -2737,7 +2806,7 @@ def _get_planets_by_systemcode(self, code: str) -> list[dict[str, any]]: Returns: list[dict[str, any]]: A list of planets matching the system code. """ - return self.planets_by_system.get(code.lower(), []) if code else [] + return self.planets_by_system.get(code, []) if code else [] def _get_tradeports_by_systemcode(self, code: str) -> list[dict[str, any]]: """Returns all tradeports with the specified system code. @@ -2748,7 +2817,7 @@ def _get_tradeports_by_systemcode(self, code: str) -> list[dict[str, any]]: Returns: list[dict[str, any]]: A list of tradeports matching the system code. """ - return self.tradeports_by_system.get(code.lower(), []) if code else [] + return self.tradeports_by_system.get(code, []) if code else [] def _get_tradeports_by_planetcode(self, code: str) -> list[dict[str, any]]: """Returns all tradeports with the specified planet code. @@ -2759,7 +2828,7 @@ def _get_tradeports_by_planetcode(self, code: str) -> list[dict[str, any]]: Returns: list[dict[str, any]]: A list of tradeports matching the planet code. """ - return self.tradeports_by_planet.get(code.lower(), []) if code else [] + return self.tradeports_by_planet.get(code, []) if code else [] def _get_tradeports_by_satellitecode(self, code: str) -> list[dict[str, any]]: """Returns all tradeports with the specified satellite code. @@ -2770,7 +2839,7 @@ def _get_tradeports_by_satellitecode(self, code: str) -> list[dict[str, any]]: Returns: list[dict[str, any]]: A list of tradeports matching the satellite code. """ - return self.tradeports_by_satellite.get(code.lower(), []) if code else [] + return self.tradeports_by_satellite.get(code, []) if code else [] def _get_tradeports_by_citycode(self, code: str) -> list[dict[str, any]]: """Returns all tradeports with the specified city code. @@ -2781,7 +2850,7 @@ def _get_tradeports_by_citycode(self, code: str) -> list[dict[str, any]]: Returns: list[dict[str, any]]: A list of tradeports matching the city code. """ - return self.tradeports_by_city.get(code.lower(), []) if code else [] + return self.tradeports_by_city.get(code, []) if code else [] def _get_tradeports_by_planetname(self, name: str) -> list[dict[str, any]]: """Returns all tradeports with the specified planet name. @@ -2793,7 +2862,7 @@ def _get_tradeports_by_planetname(self, name: str) -> list[dict[str, any]]: list[dict[str, any]]: A list of tradeports matching the planet name. """ planet = self._get_planet_by_name(name) - return self._get_tradeports_by_planetcode(planet["code"]) if planet else [] + return self._get_tradeports_by_planetcode(planet["id"]) if planet else [] def _get_tradeports_by_satellitename(self, name: str) -> list[dict[str, any]]: """Returns all tradeports with the specified satellite name. @@ -2806,7 +2875,7 @@ def _get_tradeports_by_satellitename(self, name: str) -> list[dict[str, any]]: """ satellite = self._get_satellite_by_name(name) return ( - self._get_tradeports_by_satellitecode(satellite["code"]) + self._get_tradeports_by_satellitecode(satellite["id"]) if satellite else [] ) @@ -2821,19 +2890,7 @@ def _get_tradeports_by_cityname(self, name: str) -> list[dict[str, any]]: list[dict[str, any]]: A list of tradeports matching the city name. """ city = self._get_city_by_name(name) - return self._get_tradeports_by_citycode(city["code"]) if city else [] - - def _get_tradeports_by_cityname(self, name: str) -> list[dict[str, any]]: - """Returns all tradeports with the specified city name. - - Args: - name (str): The name of the city. - - Returns: - list[dict[str, any]]: A list of tradeports matching the city name. - """ - city = self._get_city_by_name(name) - return self._get_tradeports_by_citycode(city["code"]) if city else [] + return self._get_tradeports_by_citycode(city["id"]) if city else [] def _get_tradeports_by_systemname(self, name: str) -> list[dict[str, any]]: """Returns all tradeports with the specified system name. @@ -2845,4 +2902,5 @@ def _get_tradeports_by_systemname(self, name: str) -> list[dict[str, any]]: list[dict[str, any]]: A list of tradeports matching the system name. """ system = self._get_system_by_name(name) - return self._get_tradeports_by_systemcode(system["code"]) if system else [] + return self._get_tradeports_by_systemcode(system["id"]) if system else [] + diff --git a/templates/skills/uexcorp/default_config.yaml b/templates/skills/uexcorp/default_config.yaml new file mode 100644 index 00000000..a5a2c7f0 --- /dev/null +++ b/templates/skills/uexcorp/default_config.yaml @@ -0,0 +1,197 @@ +name: UEXCorp +module: skills.uexcorp.main +description: + en: Use the UEXCorp API to get live trading and lore information about ships, locations, commodities and more in Star Citizen. + de: Nutze die UEXCorp API, um live Handels- und Lore-Informationen über Schiffe, Orte, Rohstoffe und mehr in Star Citizen zu erhalten. +# hint: +# en: +# de: +examples: + - question: + en: Please provide me a the best two trading routes for my Caterpillar, Im currently at Hurston. + de: Bitte nenne mir die zwei besten Handelsrouten für meine Caterpillar, die derzeit in Hurston steht. + answer: + en: You have two highly profitable trading routes available. The first route involves transporting Recycled Material Composite from Pickers Field on Hurston to Orison - Trade & Development Division on the planet Crusader, offering a profit of 2,148,480 aUEC for 576 SCU of cargo. The second route entails shipping Laranite from HDMS-Lathan on the satellite Arial back to Central Business District in Lorville, resulting in a profit of 297216 aUEC for the same cargo capacity. + de: Du hast zwei hochprofitable Handelsrouten zur Verfügung. Auf der ersten Route transportierst du Recyclingmaterial von Pickers Field auf Hurston nach Orison - Trade & Development Division auf dem Planeten Crusader und erzielst einen Gewinn von 2.148.480 AUEC für 576 SCU Fracht. Auf der zweiten Route wird Laranit von HDMS-Lathan auf dem Satelliten Arial zurück zum Central Business District in Lorville transportiert, was einen Gewinn von 297216 AUEC für die gleiche Frachtkapazität bedeutet. + - question: + en: I got 3000 SCU of Hydrogen loaded in my Hull-C, where can I sell it? + de: Ich habe 3000 SCU Hydrogen in meinem Hull-C geladen. Wo kann ich ihn verkaufen? + answer: + en: You can sell the 3000 SCU of Hydrogen at Baijini Point, located on ArcCorp in the Stanton system. + de: Du kannst die 3000 SCU Hydrogen am Baijini Point auf ArcCorp im Stanton-System verkaufen. + - question: + en: What can you tell me about the Hull-C? + de: Was kannst du mir über den Hull-C erzählen? + answer: + en: The Hull-C is manufactured by Musashi Industrial & Starflight Concern and falls under the 'HULL' series. It serves as a freighter and has a cargo capacity of 4608 SCU. The ship can be purchased at New Deal in Lorville, Hurston for 15,750,000 aUEC. It can accommodate a crew of 1-4 and is designed for trading on suitable space stations with a cargo deck. + de: Die Hull-C wird von Musashi Industrial & Starflight Concern hergestellt und gehört zur "HULL"-Serie. Sie dient als Frachter und hat eine Frachtkapazität von 4608 SCU. Das Schiff kann bei New Deal in Lorville, Hurston für 15.750.000 AUEC erworben werden. Es kann eine Besatzung von 1-4 Personen aufnehmen und ist für den Handel mit geeigneten Raumstationen mit einem Frachtdeck ausgelegt. + - question: + en: What is the difference between the Mole and Prospector? + de: Was ist der Unterschied zwischen dem Maulwurf und dem Prospector? + answer: + en: The MOLE is a mining ship manufactured by ARGO Astronautics, with a capacity for 2-4 crew members and a cargo hold of 96 SCU. It is flyable since version 3.8.0 and is available for purchase at Lorville on Hurston for 5,130,500 aUEC. On the other hand, the Prospector, manufactured by Musashi Industrial & Starflight Concern, is a smaller mining vessel designed for a single crew member, with a cargo capacity of 32 SCU. It is flyable since version 3.0.0 and can be purchased at Lorville for 2,061,000 aUEC. The Prospector is also available for rent at various locations in the Stanton system, unlike the MOLE. + de: Die MOLE ist ein von ARGO Astronautics hergestelltes Bergbauschiff mit einer Kapazität für 2-4 Besatzungsmitglieder und einem Laderaum von 96 SCU. Sie ist seit Version 3.8.0 flugfähig und kann bei Lorville auf Hurston für 5.130.500 AUEC gekauft werden. Der Prospector, hergestellt von Musashi Industrial & Starflight Concern, ist ein kleineres Bergbauschiff für ein einziges Besatzungsmitglied und hat eine Ladekapazität von 32 SCU. Er ist seit Version 3.0.0 flugfähig und kann auf Lorville für 2.061.000 AUEC gekauft werden. Im Gegensatz zur MOLE kann der Prospector auch an verschiedenen Orten im Stanton-System gemietet werden. + - question: + en: What do you know about the commodity Neon? + de: Was weißt du über die Ware Neon? + answer: + en: The commodity 'Neon' is categorized as a drug and is available for purchase at Nuen Waste Management for 7,000 aUEC. It is also sellable at Grim HEX for 8,428 aUEC and Jumptown for 8,805 aUEC. However, it's important to note that Neon is illegal, and engaging in activities involving this commodity may lead to fines and a crimestat. It's advisable to avoid ship scans to mitigate the risk of penalties. + de: Die Ware "Neon" wird als Droge eingestuft und kann bei Nuen Waste Management für 7.000 AUEC gekauft werden. Außerdem kann es bei Grim HEX für 8.428 AUEC und bei Jumptown für 8.805 AUEC verkauft werden. Es ist jedoch wichtig zu wissen, dass Neon illegal ist und dass der Handel mit dieser Ware zu Geldstrafen und einer Strafe führen kann. Es ist ratsam, Schiffsscans zu vermeiden, um das Risiko von Strafen zu minimieren. + - question: + en: What is the best place to buy Laranite? + de: Wo kann ich Laranit am besten kaufen? + answer: + en: The best place to buy Laranite for your Caterpillar is at ArcCorp Mining Area 045, located on the satellite Wala in the Stanton system. It's available for purchase at 2,322 aUEC per SCU. + de: Der beste Ort, um Laranit für deine Caterpillar zu kaufen, ist das ArcCorp-Bergbaugebiet 045, das sich auf dem Satelliten Wala im Stanton-System befindet. Du kannst es für 2.322 AUEC pro SCU kaufen. + - question: + en: Where is the satellite Wala located? + de: Wo befindet sich der Satellit Wala? + answer: + en: The satellite Wala is located in the ArcCorp system, orbiting the planet ArcCorp. It hosts various trading options, including locations like ArcCorp Mining Area 045, ArcCorp Mining Area 048, ArcCorp Mining Area 056, ArcCorp Mining Area 061, Shady Glen Farms, and Samson & Son's Salvage Center. + de: Der Satellit Wala befindet sich im ArcCorp-System und umkreist den Planeten ArcCorp. Er beherbergt verschiedene Handelsmöglichkeiten, darunter Orte wie ArcCorp Mining Area 045, ArcCorp Mining Area 048, ArcCorp Mining Area 056, ArcCorp Mining Area 061, Shady Glen Farms und Samson & Son's Salvage Center. +prompt: | + You have tools to access the UEXcorp API which you can use to retrieve live trading data and additional information about ships, locations, commodities and more in Star Citizen. + Here are some examples when to use the different tools at your disposal: + + Do not use markdown formatting (e.g. **name**) in your answers, but prefer lists to show multiple options or information. + Do not (never) translate any properties when giving them to the player. They must stay in english or untouched. + Only give functions parameters that were previously clearly provided by a request. Never assume any values, not the current ship, not the location, not the available money, nothing! Always send a None-value instead. + If you are not using one of the definied functions, dont give any trading recommendations. + If you execute a function that requires a commodity name, make sure to always provide the name in english, not in german or any other language. + Never mention optional function (tool) parameters to the user. Only mention the required parameters, if some are missing. + + Samples when to use function "get_trading_routes": + - "Best trading route": Indicates a user's intent to find the best trading route. + - "Trade route": Suggests the user is seeking information on trading routes. + - "Profitable trade route": Implies a focus on finding profitable trading routes. + - "Trading advice": Indicates the user wants guidance on trading decisions. + + Samples when to use function "get_locations_to_sell_to": + - "Sell commodity": Indicates a user's intent to sell a specific item. + - "Best place to sell": Suggests the user is seeking information on optimal selling locations. + - "Seller's market": Implies a focus on finding favorable selling conditions. + - "Selling advice": Indicates the user wants guidance on selling decisions. + - "Seller's guide": Suggests a request for assistance in the selling process. + - "Find buyer": Indicates the user's interest in locating potential buyers. + - "Sell item": Implies a user's intent to sell an item. + - "Sell cargo": Suggests a focus on selling cargo or goods. + - "Offload inventory": Signals the intention to sell available inventory. + + Samples when to use function "get_locations_to_buy_from": + - "Buy commodity": Indicates a user's intent to purchase a specific item. + - "Best place to buy": Suggests the user is seeking information on optimal buying locations. + - "Buyer's market": Implies a focus on finding favorable buying conditions. + - "Purchase location": Signals interest in identifying a suitable location for buying. + - "Buying advice": Indicates the user wants guidance on purchasing decisions. + - "Buyer's guide": Suggests a request for assistance in the buying process. + + Samples when to use function "get_location_information": + - "Location information": Indicates a user's intent to gather information about a specific location. + - "Location details": Suggests the user is seeking detailed information about a specific location. + + Samples when to use function "get_ship_information": + - "Ship information": Indicates a user's intent to gather information about a specific ship. + - "Ship details": Suggests the user is seeking detailed information about a specific ship. + + Samples when to use function "get_ship_comparison ": + - "Ship comparison": Indicates a user's intent to compare two ships. And everytime at least two ships are mentioned in the request. + + Samples when to use function "get_commodity_information": + - "Commodity information": Indicates a user's intent to gather information about a specific commodity. + - "Commodity details": Suggests the user is seeking detailed information about a specific commodity. + - "Commodity prices": Implies a focus on obtaining current prices for a specific commodity. + + Samples when to use function "reload_current_commodity_prices" (Great to use before retrieving sell, buy and trade options): + - "Update commodity prices": Indicates a user's intent to update the commodity prices. + - "Get current prices": Suggests the user is seeking the current commodity prices. + - "Refresh prices": Implies a focus on updating the commodity prices. +custom_properties: + - id: uexcorp_api_url + name: API URL + hint: The URL of the UEX corp API. + value: https://uexcorp.space/api/2.0/ + required: true + property_type: string + - id: uexcorp_api_timeout + name: API Timeout + hint: The timeout for the UEX corp API in seconds. (If set below 3s, 3s will be used.) + value: 10 + required: true + property_type: number + - id: uexcorp_api_timeout_retries + name: API Timeout Retries + hint: How often the request should be retried in case of a timeout. (Timeout setting may increase automatically on each retry.) + value: 3 + required: true + property_type: number + # Set this option to "true" to enable caching of the UEX corp API responses. This is recommended, as the API key's quota is very limited. + # If you set this option to "false", the Wingman will fetch all data from the UEX corp API on every start. + # If you want to update the prices, just tell the Wingman to do so. + # If all data should be fetched again, delete the cache file. (wingman_data\uexcorp\cache.json) + - id: uexcorp_cache + name: Enable Cache + hint: Set this option to "true" to enable caching of the UEX corp API responses. This is recommended, as the API key's quota is very limited. + value: true + required: true + property_type: boolean + + # Set this option to the amount of seconds you want to cache the UEX corp API responses. + # We recommend a day ("86400"), as the ship, planet, etc. information does not change that often. + - id: uexcorp_cache_duration + name: Cache Duration + hint: Set this option to the amount of seconds you want to cache the UEX corp API responses. We recommend a day ("86400"). + value: 86400 + required: true + property_type: number + + # Set this option to "true" to show only one of the most profitable routes for each commodity. + # Set this option to "false" to show all routes. This may include multiple routes for the same commodity. + # Recommended: "true" + - id: uexcorp_summarize_routes_by_commodity + name: Summarize Routes by Commodity + hint: Set this option to "true" to show only the most profitable routes per commodity. "false" shows multiple options per commodity. + value: true + required: true + property_type: boolean + + # Set this option to "true" to make the start location for trade route calculation a mandatory information. + # Set this option to "false" to make the start location for trade route calculation a optional information. + # If "false" and no start location is given, all tradeports are taken into account. + - id: uexcorp_tradestart_mandatory + name: Trade Start Mandatory + hint: Set this option to "true" to make the start location for trade route calculation a mandatory information. If "false" and no start location is given, all tradeports are taken into account. + value: true + required: true + property_type: boolean + + # Use this to blacklist certain trade ports or commodities or combinations of both. + # Default value is '[]', which means no trade ports or commodities are blacklisted. + # If we want to add a trade port to the blacklist, we add something like this: {"tradeport":"Baijini Point"} This will blacklist the trade port completely from trade route calculations. + # If we want to add a commodity to the blacklist, we add something like this: {"commodity":"Medical Supplies"} This will blacklist the commodity completely from trade route calculations. + # If we want to add a combination to the blacklist, we add something like this: {"tradeport":"Baijini Point", "commodity":"Medical Supplies"} This will blacklist this commodity for the given trade port. + # If we want to add multiple trade ports or commodities or combinations of both, we add them in a list like this: [{"tradeport":"Baijini Point", "commodity":"Medical Supplies"}, {"commodity":"Medical Supplies"}, {"tradeport":"Port Tressler"}] + # This value is a JSON string, if you have created a list, use a JSON validator like https://jsonlint.com/ to check if the list is valid. + - id: uexcorp_trade_blacklist + name: Trade Blacklist + hint: JSON string to blacklist certain trade ports or commodities or combinations of both. Default value is empty ('[]'). Sample -> [{"tradeport":"Baijini Point", "commodity":"Medical Supplies"}, {"commodity":"Medical Supplies"}, {"tradeport":"Port Tressler"}] + value: "[]" + required: true + property_type: string + + # Set this option to the amount of trade routes you want to show at default. + # You can always tell Wingman AI to show more or less trade routes for one request, if that number is not given, this setting is used. + - id: uexcorp_default_trade_route_count + name: Default Trade Route Count + hint: Set this option to the amount of trade routes you want to show at default. + value: 1 + required: true + property_type: number + + # Set this option to true to take estimated scu availability into account for trade route calculations. + # This will reduce the amount of trade routes shown, but will give you more accurate results. + - id: uexcorp_use_estimated_availability + name: Use Estimated Availability + hint: Enable this option to take estimated scu availability into account for trade route calculations. + value: true + required: true + property_type: boolean diff --git a/templates/skills/uexcorp/logo.png b/templates/skills/uexcorp/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..94392d3f1d3e3e729f20082a4ed46d5995eb7853 GIT binary patch literal 7506 zcmd6MWmjC?vUTI`?hptL-B^OV2MDeSjl0vhOK^f)AXpP5SRlAtBOx^I0fGidaBu9A zbDsB{`~HKw_E@`S)tWVH)gEJis#r}8B|L0uYybd&r=l#c{bWP`ZcOy2cfX|h=#xRU zl~I=g0P2!(?#$7id`3%UZFK;^pY@3s2>>Ae@^=9MFAxB*Zw3H}W&;2eZh385;!g*W z)`luJ>goW_CyWU|Lm~#CJRzi~2Y^Hk_^bAW0IEnd|H0Zw?Ei3(0f1>hJnu*|3!f~ zN<1@E*JPA)^{{4q!3*LAJ(I*{WMmZgu(A==mRI~I{mGMfW(R?|i2{K>K0drY0=%vs zwm?1+5fLDWAIQ(o^Tgos^mBoj`|`MWGXGoT|LMqEds=$fyFu(-T^RrBnp?PfK_s3% z`&;Ng>)-c;*xUTKl8fg*Z9O#z{CfiA;{^f#qy3aB{x>T6+QZ)Zsq$ZaNj~v^G5^2V zKXt@`e~bUG%ly03e?^~KmBbbY{^zqvVpEx}V*miu(<<^Zy1qyUc{u5MvwlP4LGXxm zAyrh^0%tGxzA7n0BG64xpZdVq0eWz%z|I*VPpz!ZP+_iJQ5sK9OwSn?o?e2*{dSbR zda@)&F}AlbF;>2uTU{9gEovUu=TQ9c;Hdl8pZ?aXe6cI|Pfst`xty=#t7{LJSNGkn zA-C>u*x4HoOkq@7WPGIPP`MtapM-Azr`#|CsxU*L`{ss*f!CsYCFfm1x41$4Ip0@; zZx7zq8FGWwZyoGUZvvf0QlIbT=jXR9CD4lSKVB}o`b>t@7}IC${>*T4a#F*k6BV?% zsF*2zbH7k$8Bp7Kxm4qE!THDtslNxBcC@f2tRyqZwWZX~&k^k7o9&fHX*=DVZda7I z^`?)2-V7eY4w9M*8Xf0Z^=sZ3RZ7PBF&vyl;nAdSFu;_&jtvE!ngocP9oq-qd)W33uvm9#y(bR9^@QT^L41w7&A??caR3J?bW1{nf7o zzi@DL{0$Yb`M!%kt-Bl@y4dJwKulNPz{k9vqC_vknmd;N?d%9T&W`w;QZsu>O6M%U z2EWjOtX0)p+?+ePw;pxf@5cZ&EIX z(#rHKH_XLRb_SFKpXohF+FEnyrKp%#vwJtv#EaX85BRF29ETV=Q*T(sfhJ8(g41&a z(D{O%-vV!jO0_wF^hvnFu&Qs8K3-lN15)p`T=OJ@5KQqAv$4UiOV)(UUUGbo^fuYM zMdZ46-DHIAlI|3B92^xz(^JRs(3jQ%jXy@dWldXzon`eLM%Py?X_|(>h%*LyK*hO!}Id;!V|6^qTSomFlw~O z{6sQ5{EJQ$VnF+rL1&zMZiy62`L?hRPBO*I^G%wT!4O&Pni%Pj- zi}<;x2Gbrnv)DE(t50WHs8pH34Va=dru+})^!@k4*9S9rSxgF*UpTijfExPVc4xvz zVC_fNj$HQ*bVWIQ&}k^ri$R2bmdu)Ynp}rCJb-uI;2tfut1J8mf3V0$>R4Kz&q#=K zTvRbcVnyQGqOOzRJG5%@?fRU_XeVuNC>&aFV~m1zWbsFpw#RlK>8Eh^o0xFU&nKg^ zVF@Y!GN)`Z2}QnS(1kys?SNoY9axM3@^8_!{ZtECJDlCzVOMQyK6E?-wV zeb44Aba9$ z29|bgFW~@TPgP3gszFUC{@`i2yiarNwdK=qCB^i9b0`UDMt?r_8l6A_J=hZVN*vbC zo)dg*5#}GhPiYjVDU)h2dL8P=ZBoD!xjpLsgIBgQ`?)=voF1NAPmAo}=@#!7rOiki zSR(z<wANju&9cO#tPF$hgmm%wi|EuUVM=av%(7Lg<$Q(9uC{>InOY2 zJwgpZ?S3~cT~j(5Nt%}~37?Oe6VS9|!{f@;z={BHW-5o`pn>p=EGBs_=1{@lHMTmx zqQZKS$dZ*fIW9l-y~W^35;~k(>FDT(Z^aPvq&2Tz`;k zAfCCmt3@SZ)94OJzAvaUi}d2j!*?@CL9lD^tLEFQi>0<{T8FvIZl@R-mh3_^+bf!{ z2)J#4Uv0V>OJ6{| zfCWjF$%d0FMo>lPj{Y+ZF2#=2x5-U?7yI!--(;j$RUp$)d@~3E+q+Hfd&ea)1v2@1 zS_3TemXxvO3uHyQexW(3B=$+V)y~u~p+56nQrSYXamW=!XEm~ihL4FAMr7L zHfrQv(qF`I`hyk5L2wyjB#kY41VP7#7Rm4lBLR#u5q%FG=b%O^@!+~QMGumdwh|S1 z#9QfqBjw;|=QxHxYC~X!za@6*H6Xly$!f)V-qSu|yg{3?yXF+8`^jzqS@Wz5xBQiO zF!ph?`@G<%Up*}oC%dzD@q?J1;XGF*J3BkSVw^-HM!2YB7&-T>skV)H{MR3?!B_0s z8Qb?4Bjdt#D`nv;?LHK{S6Z=v)ardyH*~eoJVo|7clXvN5h}1J4~7hm?Vk_sN>$G( zCAODz-_{6VSnbW~mMA8)bx2gmH&Hac=s9^0Q+|%4W7in4U(%Jyeo2>S#~+B#pI8#Z zTK}pRegrc(dtu9fRs811<-3z3^A&;jbg$7Cf8t~eN9O* zTf_xs$B$r^kFlpT!}P%R{p!eJp{Z1|Yb6Mdk{q}A;kSE3dW5+Y)KBAa>W=ICf*_UN~smF3 z3|XGkR=Uj`YV8?$+BUwtD=(^$n15*S!G+f*rkEw9G3MC zFn*sPenO?Z!Sh3Lj-%43d}Sa2UmZmYpE0vBVMha3ENr6deN0`>?g{dCcW*2tJ%~2z zy$5hTM(mx=NNc)aamD`f@)`HgyzwrmBTD*;b)2hJoR-~J?6YldamUbHP_mDJ1(#QO z>krm+x}o8Eu0LjB*1v;}9h{}uZ0**+P47f$GBwPxG*^5GOz4g#T9uBdk0;xApqix`TTISRoDO_P*p;l= zO$iP8*c_)b!2!!1!fhLM+1Z_^2KTX#&*H5R9>%4fWosK7>9F|9851HDYR|RYLnZAw z@6f(~DH^-QSog}tdw_--=5ts@6KIR63jjH-}XrkhMp z8^L9%zB4lB9=hq~S~VUud~;5#u}z64cv^BLuwKpYu<#{iMdke$lw)p2HB{-=!bD)L z1OX_TA#qU=2QiYpB)CbCds0Z1pw>l@l+AMYul*Qz&tH*kIBrb`KsujOHsxvq z>G}tc6MN?SUJb3ysF0Te9F;E^svZJ=BIn2ga>?o?0YVvmK%I#+FQgTocbJy>tLxpi zu{U>VAhFl$A9W8q`p(%pCY9PaRIP^{mv*;Y)*flv%B3cDX#jeO{fi&cdx_dOj;1xx z6e?WBNy+w!k`AP^HKgYy8waGj3W>>oB{1aVG5aczEjELLmxTk1hjmNw&*5)J0`q>7 z>~k?a=b#X5^4Qa$u9XTP4oC>Bc;oQuib;TslhoGU$kNz@)NT%mnlMP<(+zw)^3n0V z@CRSLT#)}YPDOy9D_>o8wK|(O+Oo_E6!#)A1u{REil?TYIL9}I(m7^?;UzkyCpf`% zNUT+;E?Y?l(kzUf#L*7k#?fTR0|IDO9-Rq~G6+k|8jR2}latM6=GVD* zA_$imG1L3@8N`)GTrG^qa;T1qB+pj1rj#NqRA~U z@Z7zp3s=YH#lT&@0E4$TKbPZxk1uqQxG7-#E2r-dO*qqD2a zpAgHWuXwZ@iO19ycy&t)F@`}-x=y}M22QV>+q^mByER;S>g|YxV+Q7c9QVSI)XJbl zq-VTF=Nxn6lniw36U?wVo{{h)EZktSC`k-9JK3c8R`@#`Vw~Pj$KTu$6)!`V@UnM_ zn2NjR&wCEWlRvr2(8!b&6W4wzGO}F}Tuor4m?FAIi{Spw2`o(6i<+%80Cw(`=8F0* zGRCLNC%7@wIiGUp^C7ZX>HYDivylQ@}Pb4$wVYgZ<#T5i@2Bb|c0M?Z*Bzj!ybEmf^EGs3AxOUxvhSm%(8ySr=AoWcC6iYzlRNw8;8rD;aQb^IZ~t`FIM1Jt3^$^)Rv$5x(n#a|cFk5Yh3PnTBY_7v0mTu=gdpvUKY(AIoGq%o~(%=Ru|tXN6HsbouUcm(BZlj0E0oKH|Gaf zp{sRl{)N=sMk8h0S9;;f<$%ItC)!Yq29$|h>DzEcwS#s5NDSv5ew?UTe=L9;%7?;; zHMUk~`*QFKBgu^`C)h2aWX{N}C5<6B5DViG3-SSf%M5KAXPg5xN*J~ugB&t&#=7)7 z^qah9zIvPoK#vl7(*N!SD#BoA>c%&(qoG!BeT~JG*H*nOP_RkiSDa1XrSBAz7v6e$ofn(MDk}qD45?ZQ1PR%4NuBe zh;NsHb=L{=>p!|c6a>-CCQT$oOd-MK5Ja8NtQ?FI`<-P`Rg@_@pDo&#LTxgoXIcyX z0vkQ4@v@pA8A)UN9Tcv}3AL^M$+E@K1^sWK9OEQESZ^UZ2OxCjvemtw_ z3}emL*JpLey+;v;L>2)xujtJ(wPoTE+1U%w^= z!Tckw%hQ!02eaN&kIkUAo5F})mbXgPAVVQRWyk)_erdA_4up6V5m^ZDi@I~&se z5T0v|Xj3ie`)@6*H$iL@CgUU%SEz@sdrW!Z=orMB`<*o^98p(LDXHgWM)R?VT+Tf64eoi8EuCeKz_A zO6Bo@!gv^33#rpEyN-Mq#yXE}xN(oMA_c`!M|37DwI|d?9)A+!sK74Ry^lW_&bN%r77+lBB?_X!6j z?b91SBaCBcs8)0O%v+?+q2OwA1saq&P67!(R~D+sgB2@3nRhNQ1QSl;${V@d6d-KQ+7iD^MTv z*$O=t(8l znoMN9sGeAhD{2pv%OzrP*&Oe-C)cE*a`yCMl_7pql{Th&f9V3qG~E^H|2CLjLYIg? ztXm?fnws=(NiVgrq?(8E+h^yH1F|5&ixI}4pNw4AnF^2ijGVMRcj9?T*xr4+oQm7@ zdD7Sp%aF*l7`a%DnbuZz{9kgqXhtXd79Yx=r#D$n04)}rVU>Tv$;F3d8bEzU6imy_ z$z#-dyIC2v+2f1_>R^6BKtZur+BN8uOoSN^{3A(fjEsU{t930ndDJYF=Z_h%XMp^2 zxhPK>2bA}df_+(jA%Lc^v61L26ixJJFQ$fodWRfC7U-zkaO7Iwt{v2eb?$flF#*^4 z)hCml<*6>PPw3&G`Iz{3X)QaK`C;b8VT!}0ys8cM&Ru^^GH+7?61{Jp55>x~cMkxn zxLoDOmeOZdeBC%bnWD5fyPrJ(TFaW>FE_Im?zwkb87x3SmO%QA@^`s;GT$mThqva@ z8VF$n5%*oeW+$|vc`6K2)3rwZx4)Hbl(5f?r8$VrB5#Bz3y5VdNz^Bn4SR<%ZYD*4 zr_kSeD1x^P4DVYZ%rH`(idD78;{D>PB*ZYdvuSqPQF!0m`W$Y0<&X+TmXB870sy~Y z&RC|qEUDyNVGkeU5T*R-!K7C!{UF63gWIpX+&^cHEj?}_cJ*E{Prvw~T)_2mR}J1e@EO=}j`J)ZFz9|U`XeMovr@l$iYbHEfJlv^{};md+FniTmW(UQX&`u-rP4_xf~x4}xi!M1SWi?G zy|bJSzZ(DzRDZ```UMyB^Dv{-3gy|}qioBEJL80p6LZtRtTR91^{WNW*JvhZkDR;o zHd5>ugeX`&kAJirnhBo9pSPS^2`db$s1==aQQv+A!2Y$aO8`NQoPe@K_cFv4`ro(zGk*~ None: + super().__init__( + config=config, wingman_config=wingman_config, settings=settings + ) + + self.data_path = get_writable_dir(path.join("skills", "uexcorp", "data")) + self.logfileerror = path.join(self.data_path, "error.log") + self.logfiledebug = path.join(self.data_path, "debug.log") + self.cachefile = path.join(self.data_path, "cache.json") + + self.skill_version = "v12" + self.skill_loaded = False + self.skill_loaded_asked = False + + # init of config options + self.uexcorp_api_url: str = None + self.uexcorp_api_key: str = None + self.uexcorp_api_timeout: int = None + self.uexcorp_api_timeout_retries: int = None + self.uexcorp_cache: bool = None + self.uexcorp_cache_duration: int = None + self.uexcorp_summarize_routes_by_commodity: bool = None + self.uexcorp_tradestart_mandatory: bool = None + self.uexcorp_trade_blacklist = [] + self.uexcorp_default_trade_route_count: int = None + self.uexcorp_use_estimated_availability: bool = None + + # init of data lists + # self.status_codes = [] + + self.ships = [] + self.ship_names = [] + self.ship_dict = {} + self.ship_code_dict = {} + + self.commodities = [] + self.commodity_names = [] + self.commodity_dict = {} + self.commodity_code_dict = {} + + self.systems = [] + self.system_names = [] + self.system_dict = {} + self.system_code_dict = {} + + self.tradeports = [] + self.tradeport_names = [] + self.tradeport_dict = {} + self.tradeport_code_dict = {} + self.tradeports_by_system = collections.defaultdict(list) + self.tradeports_by_planet = collections.defaultdict(list) + self.tradeports_by_satellite = collections.defaultdict(list) + self.tradeports_by_city = collections.defaultdict(list) + + self.planets = [] + self.planet_names = [] + self.planet_dict = {} + self.planet_code_dict = {} + self.planets_by_system = collections.defaultdict(list) + + self.satellites = [] + self.satellite_names = [] + self.satellite_dict = {} + self.satellite_code_dict = {} + self.satellites_by_planet = collections.defaultdict(list) + + self.cities = [] + self.city_names = [] + self.city_dict = {} + self.city_code_dict = {} + self.cities_by_planet = collections.defaultdict(list) + + self.location_names_set = set() + + self.cache_enabled = True + self.cache = { + "function_args": {}, + "search_matches": {}, + "readable_objects": {}, + } + + self.dynamic_context = "" + + async def _print(self, message: str | dict, is_extensive: bool = False, is_debug: bool = True) -> None: + """ + Prints a message if debug mode is enabled. Will be sent to the server terminal, log file and client. + + Args: + message (str | dict): The message to be printed. + is_extensive (bool, optional): Whether the message is extensive. Defaults to False. + + Returns: + None + """ + if (not is_extensive and self.settings.debug_mode) or not is_debug: + await self.printr.print_async( + message, + color=LogType.INFO, + ) + elif self.DEV_MODE: + with open(self.logfiledebug, "a", encoding="UTF-8") as f: + f.write(f"#### Time: {datetime.now()} ####\n") + f.write(f"{message}\n\n") + + + def _log(self, message: str | dict, is_extensive: bool = False) -> None: + """ + Prints a debug message (synchronously) only on the server (and in the log file). + + Args: + message (str | dict): The message to be printed. + is_extensive (bool, optional): Whether the message is extensive. Defaults to False. + + Returns: + None + """ + if not is_extensive and self.settings.debug_mode: + self.printr.print( + message, + color=LogType.INFO, + server_only=True, + ) + elif self.DEV_MODE: + with open(self.logfiledebug, "a", encoding="UTF-8") as f: + f.write(f"#### Time: {datetime.now()} ####\n") + f.write(f"{message}\n\n") + + def _get_function_arg_from_cache( + self, arg_name: str, arg_value: str | int = None + ) -> str | int | None: + """ + Retrieves a function argument from the cache if available, otherwise returns the provided argument value. + + Args: + arg_name (str): The name of the function argument. + arg_value (str | int, optional): The default value for the argument. Defaults to None. + + Returns: + dict[str, any]: The cached value of the argument if available, otherwise the provided argument value. + """ + if not self.cache_enabled: + return arg_value + + if arg_value is None or ( + isinstance(arg_value, str) and arg_value.lower() == "current" + ): + cached_arg = self.cache["function_args"].get(arg_name) + if cached_arg is not None: + self._log( + f"'{arg_name}' was not given and got overwritten by cache: {cached_arg}" + ) + return cached_arg + + return arg_value + + def _set_function_arg_to_cache( + self, arg_name: str, arg_value: str | int | float = None + ) -> None: + """ + Sets the value of a function argument to the cache. + + Args: + arg_name (str): The name of the argument. + arg_value (str | int, optional): The value of the argument. Defaults to None. + """ + if not self.cache_enabled: + return + + function_args = self.cache["function_args"] + old_value = function_args.get(arg_name, "None") + + if arg_value is not None: + self._log( + f"Set function arg '{arg_name}' to cache. Previous value: {old_value} >> New value: {arg_value}", + True, + ) + function_args[arg_name] = arg_value + elif arg_name in function_args: + self._log( + f"Removing function arg '{arg_name}' from cache. Previous value: {old_value}", + True, + ) + function_args.pop(arg_name, None) + + async def validate(self) -> list[WingmanInitializationError]: + errors = await super().validate() + + self.uexcorp_api_key = await self.retrieve_secret( + "uexcorp", errors, "You can create your own API key here: https://uexcorp.space/api/apps/" + ) + self.uexcorp_api_url = self.retrieve_custom_property_value( + "uexcorp_api_url", errors + ) + self.uexcorp_api_timeout = self.retrieve_custom_property_value( + "uexcorp_api_timeout", errors + ) + self.uexcorp_api_timeout_retries = self.retrieve_custom_property_value( + "uexcorp_api_timeout_retries", errors + ) + self.uexcorp_cache = self.retrieve_custom_property_value( + "uexcorp_cache", errors + ) + self.uexcorp_cache_duration = self.retrieve_custom_property_value( + "uexcorp_cache_duration", errors + ) + self.uexcorp_summarize_routes_by_commodity = ( + self.retrieve_custom_property_value( + "uexcorp_summarize_routes_by_commodity", errors + ) + ) + self.uexcorp_tradestart_mandatory = self.retrieve_custom_property_value( + "uexcorp_tradestart_mandatory", errors + ) + self.uexcorp_default_trade_route_count = self.retrieve_custom_property_value( + "uexcorp_default_trade_route_count", errors + ) + self.uexcorp_use_estimated_availability = self.retrieve_custom_property_value( + "uexcorp_use_estimated_availability", errors + ) + + trade_backlist_str: str = self.retrieve_custom_property_value( + "uexcorp_trade_blacklist", errors + ) + if trade_backlist_str: + try: + self.uexcorp_trade_blacklist = json.loads(trade_backlist_str) + except json.decoder.JSONDecodeError: + errors.append( + WingmanInitializationError( + wingman_name=self.name, + message="Invalid custom property 'uexcorp_trade_blacklist' in config. Value must be a valid JSON string.", + error_type=WingmanInitializationErrorType.INVALID_CONFIG, + ) + ) + + try: + await self._start_loading_data() + except Exception as e: + errors.append( + WingmanInitializationError( + wingman_name=self.name, + message=f"Failed to load data: {e}", + error_type=WingmanInitializationErrorType.UNKNOWN, + ) + ) + + return errors + + async def _load_data(self, reload_prices: bool = False, callback = None) -> None: + """ + Load data for UEX corp wingman. + + Args: + reload (bool, optional): Whether to reload the data from the source. Defaults to False. + """ + + if reload_prices: + await self._load_commodity_prices() + await self._save_to_cachefile() + return + + async def _load_from_cache(): + if not self.uexcorp_cache: + return + + # check file age + data = {} + try: + with open(self.cachefile, "r", encoding="UTF-8") as f: + data = json.load(f) + except (FileNotFoundError, json.decoder.JSONDecodeError): + pass + + # check file age + if ( + data.get("timestamp") + and data.get("timestamp") + self.uexcorp_cache_duration + > self._get_timestamp() + and data.get("skill_version") == self.skill_version + ): + if data.get("ships"): + self.ships = data["ships"] + if data.get("commodities"): + self.commodities = data["commodities"] + if data.get("systems"): + self.systems = data["systems"] + if data.get("tradeports"): + self.tradeports = data["tradeports"] + # fix prices keys (from string to integer due to unintentional json conversion) + for tradeport in self.tradeports: + if "prices" in tradeport: + tradeport["prices"] = { + int(key): value + for key, value in tradeport["prices"].items() + } + if data.get("planets"): + self.planets = data["planets"] + if data.get("satellites"): + self.satellites = data["satellites"] + if data.get("cities"): + self.cities = data["cities"] + + async def _load_missing_data(): + if not self.ships: + self.ships = await self._fetch_uex_data("vehicles") + self.ships = [ship for ship in self.ships if ship["game_version"]] + + if not self.commodities: + self.commodities = await self._fetch_uex_data("commodities") + self.commodities = [commodity for commodity in self.commodities if commodity["is_available"] == 1] + + if not self.systems: + self.systems = await self._fetch_uex_data("star_systems") + self.systems = [ + system for system in self.systems if system["is_available"] == 1 + ] + for system in self.systems: + self.tradeports += await self._fetch_uex_data( + f"terminals/id_star_system/{system['id']}/type/commodity/is_available/1/is_visible/1" + ) + self.cities += await self._fetch_uex_data( + f"cities/id_star_system/{system['id']}" + ) + self.satellites += await self._fetch_uex_data( + f"moons/id_star_system/{system['id']}" + ) + self.planets += await self._fetch_uex_data( + f"planets/id_star_system/{system['id']}" + ) + await self._load_commodity_prices() + + # data manipulation + planet_codes = [] + for planet in self.planets: + if planet["code"] not in planet_codes: + planet_codes.append(planet["code"]) + + for tradeport in self.tradeports: + if ( + tradeport["id_space_station"] + and len(tradeport["nickname"].split("-")) == 2 + and tradeport["nickname"].split("-")[0] in planet_codes + and re.match(r"^L\d+$", tradeport["nickname"].split("-")[1]) + ): + tradeport["id_planet"] = "" + + def _load_data(callback=None): + async def _actual_loading(callback=None): + await _load_from_cache() + await _load_missing_data() + await self._save_to_cachefile() + + if callback: + await callback() + + new_loop = asyncio.new_event_loop() + asyncio.set_event_loop(new_loop) + new_loop.run_until_complete(_actual_loading(callback)) + new_loop.close() + + threading.Thread(target=_load_data, args=(callback,)).start() + + async def _save_to_cachefile(self) -> None: + if ( + self.uexcorp_cache + and self.uexcorp_cache_duration > 0 + and self.ships + and self.commodities + and self.systems + and self.tradeports + and self.planets + and self.satellites + and self.cities + ): + data = { + "timestamp": self._get_timestamp(), + "skill_version": self.skill_version, + "ships": self.ships, + "commodities": self.commodities, + "systems": self.systems, + "tradeports": self.tradeports, + "planets": self.planets, + "satellites": self.satellites, + "cities": self.cities, + } + with open(self.cachefile, "w", encoding="UTF-8") as f: + json.dump(data, f, indent=4) + + async def _load_commodity_prices(self) -> None: + """ + Load commodity prices from UEX corp API. + + Returns: + None + """ + + self.cache["readable_objects"] = {} + + # currently the prices are saved in api v1 style to minimize rework time for now + for i in range(0, len(self.tradeports), 10): + tradeports_batch = self.tradeports[i : i + 10] + tradeport_ids = [tradeport["id"] for tradeport in tradeports_batch] + + commodity_prices = await self._fetch_uex_data( + "commodities_prices/id_terminal/" + ",".join(map(str, tradeport_ids)) + ) + + for tradeport in tradeports_batch: + tradeport["prices"] = {} + + for commodity_price in commodity_prices: + if commodity_price["id_terminal"] == tradeport["id"]: + commodity = next( + ( + commodity + for commodity in self.commodities + if commodity["id"] == commodity_price["id_commodity"] + ), + None, + ) + if commodity: + transaction_type = "buy" if commodity_price["price_buy"] > 0 else "sell" + price = { + "name": self._format_commodity_name(commodity), + "kind": commodity["kind"], + "operation": transaction_type, + "price_buy": commodity_price["price_buy"], + "price_sell": commodity_price["price_sell"], + "date_update": commodity_price["date_modified"], + "is_updated": bool(commodity_price["date_modified"]), + "scu": commodity_price[f"scu_{transaction_type}"] or None, + "scu_average": commodity_price[f"scu_{transaction_type}_avg"] or None, + "scu_average_week": commodity_price[f"scu_{transaction_type}_avg_week"] or None, + } + # calculate expected scu + count = 0 + total = 0 + if price["scu"]: + count += 2 + total += (price["scu"] * 2) + if price["scu_average"]: + count += 1 + total += price["scu_average"] + if price["scu_average_week"]: + count += 1 + total += price["scu_average_week"] + price["scu_expected"] = int(total / count) if count > 0 else None + + tradeport["prices"][commodity["id"]] = price + + async def _start_loading_data(self) -> None: + """ + Prepares the wingman for execution by initializing necessary variables and loading data. + + This method retrieves configuration values, sets up API URL and timeout, and loads data + such as ship names, commodity names, system names, tradeport names, city names, + satellite names and planet names. + It also adds additional context information for function parameters. + + Returns: + None + """ + + # fix api url + if self.uexcorp_api_url and self.uexcorp_api_url.endswith("/"): + self.uexcorp_api_url = self.uexcorp_api_url[:-1] + + # fix timeout + self.uexcorp_api_timeout = max(3, self.uexcorp_api_timeout) + self.uexcorp_api_timeout_retries = max(0, self.uexcorp_api_timeout_retries) + + await self._load_data(False, self._prepare_data) + + async def _prepare_data(self) -> None: + """ + Prepares the wingman for execution by initializing necessary variables. + """ + + self.planets = [ + planet for planet in self.planets if planet["is_available"] == 1 + ] + + self.satellites = [ + satellite + for satellite in self.satellites + if satellite["is_available"] == 1 + ] + + # remove urls from ships + for ship in self.ships: + ship.pop("url_store", None) + ship.pop("url_brochure", None) + ship.pop("url_hotsite", None) + ship.pop("url_video", None) + ship.pop("url_photos", None) + + # remove screenshot from tradeports + for tradeport in self.tradeports: + tradeport.pop("screenshot", None) + tradeport.pop("screenshot_thumbnail", None) + tradeport.pop("screenshot_author", None) + + # add hull trading option to trade ports + for tradeport in self.tradeports: + tradeport["hull_trading"] = bool(tradeport["has_loading_dock"]) + + # add hull trading option to ships + ships_for_hull_trading = [ + "Hull C", + "Hull D", + "Hull E", + ] + for ship in self.ships: + ship["hull_trading"] = ship["name"] in ships_for_hull_trading + + self.ship_names = [ + self._format_ship_name(ship) + for ship in self.ships + ] + self.ship_dict = { + self._format_ship_name(ship).lower(): ship for ship in self.ships + } + self.ship_code_dict = {ship["id"]: ship for ship in self.ships} + + self.commodity_names = [ + self._format_commodity_name(commodity) for commodity in self.commodities + ] + self.commodity_dict = { + self._format_commodity_name(commodity).lower(): commodity + for commodity in self.commodities + } + self.commodity_code_dict = { + commodity["id"]: commodity for commodity in self.commodities + } + + self.system_names = [ + self._format_system_name(system) for system in self.systems + ] + self.system_dict = { + self._format_system_name(system).lower(): system for system in self.systems + } + self.system_code_dict = { + system["id"]: system for system in self.systems + } + + self.tradeport_names = [ + self._format_tradeport_name(tradeport) for tradeport in self.tradeports + ] + self.tradeport_dict = { + self._format_tradeport_name(tradeport).lower(): tradeport + for tradeport in self.tradeports + } + self.tradeport_code_dict = { + tradeport["id"]: tradeport for tradeport in self.tradeports + } + for tradeport in self.tradeports: + if tradeport["id_star_system"]: + self.tradeports_by_system[tradeport["id_star_system"]].append(tradeport) + if tradeport["id_planet"]: + self.tradeports_by_planet[tradeport["id_planet"]].append(tradeport) + if tradeport["id_moon"]: + self.tradeports_by_satellite[tradeport["id_moon"]].append( + tradeport + ) + if tradeport["id_city"]: + self.tradeports_by_city[tradeport["id_city"]].append(tradeport) + + self.city_names = [self._format_city_name(city) for city in self.cities] + self.city_dict = { + self._format_city_name(city).lower(): city for city in self.cities + } + self.city_code_dict = {city["id"]: city for city in self.cities} + for city in self.cities: + self.cities_by_planet[city["id_planet"]].append(city) + + self.satellite_names = [ + self._format_satellite_name(satellite) for satellite in self.satellites + ] + self.satellite_dict = { + self._format_satellite_name(satellite).lower(): satellite + for satellite in self.satellites + } + self.satellite_code_dict = { + satellite["id"]: satellite for satellite in self.satellites + } + for satellite in self.satellites: + self.satellites_by_planet[satellite["id_planet"]].append(satellite) + + self.planet_names = [ + self._format_planet_name(planet) for planet in self.planets + ] + self.planet_dict = { + self._format_planet_name(planet).lower(): planet for planet in self.planets + } + self.planet_code_dict = { + planet["id"]: planet for planet in self.planets + } + for planet in self.planets: + self.planets_by_system[planet["id_star_system"]].append(planet) + + self.location_names_set = set( + self.system_names + + self.tradeport_names + + self.city_names + + self.satellite_names + + self.planet_names + ) + + self.skill_loaded = True + if self.skill_loaded_asked: + self.skill_loaded_asked = False + await self._print("UEXcorp skill data loading complete.", False, False) + + def _add_context(self, content: str): + """ + Adds additional context to the first message content, + that represents the context given to open ai. + + Args: + content (str): The additional context to be added. + + Returns: + None + """ + self.dynamic_context += "\n" + content + + def _get_timestamp(self) -> int: + """ + Get the current timestamp as an integer. + + Returns: + int: The current timestamp. + """ + return int(datetime.now().timestamp()) + + def _get_header(self): + """ + Returns the header dictionary containing the API key. + Used for API requests. + + Returns: + dict: The header dictionary with the API key. + """ + key = self.uexcorp_api_key + return {"Authorization": f"Bearer {key}"} + + async def _fetch_uex_data( + self, endpoint: str, params: Optional[dict[str, any]] = None + ) -> list[dict[str, any]]: + """ + Fetches data from the specified endpoint. + + Args: + endpoint (str): The API endpoint to fetch data from. + params (Optional[dict[str, any]]): Optional parameters to include in the request. + + Returns: + list[dict[str, any]]: The fetched data as a list of dictionaries. + """ + url = f"{self.uexcorp_api_url}/{endpoint}" + await self._print(f"Fetching data from {url} ...", True) + + request_count = 1 + timeout_error = False + requests_error = False + + while request_count == 1 or (request_count <= (self.uexcorp_api_timeout + 1) and timeout_error): + if requests_error: + await self._print(f"Retrying request #{request_count}...", True) + requests_error = False + + timeout_error = False + try: + response = requests.get( + url, + params=params, + timeout=(self.uexcorp_api_timeout * request_count), + headers=self._get_header(), + ) + response.raise_for_status() + except requests.exceptions.RequestException as e: + await self._print(f"Error while retrieving data from {url}: {e}") + requests_error = True + if isinstance(e, requests.exceptions.Timeout): + timeout_error = True + request_count += 1 + + if requests_error: + return [] + + response_json = response.json() + if "status" not in response_json or response_json["status"] != "ok": + await self._print(f"Error while retrieving data from {url}") + return [] + + return response_json.get("data", []) + + def _format_ship_name(self, ship: dict[str, any]) -> str: + """ + Formats the name of a ship. + This represents a list of names that can be used by the player. + So if you like to use manufacturer names + ship names, do it here. + + Args: + ship (dict[str, any]): The ship dictionary containing the ship details. + + Returns: + str: The formatted ship name. + """ + return ship["name_full"] + + def _format_tradeport_name(self, tradeport: dict[str, any]) -> str: + """ + Formats the name of a tradeport. + + Args: + tradeport (dict[str, any]): The tradeport dictionary containing the name. + + Returns: + str: The formatted tradeport name. + """ + return tradeport["nickname"] + + def _format_city_name(self, city: dict[str, any]) -> str: + """ + Formats the name of a city. + + Args: + city (dict[str, any]): A dictionary representing a city. + + Returns: + str: The formatted name of the city. + """ + return city["name"] + + def _format_planet_name(self, planet: dict[str, any]) -> str: + """ + Formats the name of a planet. + + Args: + planet (dict[str, any]): A dictionary representing a planet. + + Returns: + str: The formatted name of the planet. + """ + return planet["name"] + + def _format_satellite_name(self, satellite: dict[str, any]) -> str: + """ + Formats the name of a satellite. + + Args: + satellite (dict[str, any]): The satellite dictionary. + + Returns: + str: The formatted satellite name. + """ + return satellite["name"] + + def _format_system_name(self, system: dict[str, any]) -> str: + """ + Formats the name of a system. + + Args: + system (dict[str, any]): The system dictionary containing the name. + + Returns: + str: The formatted system name. + """ + return system["name"] + + def _format_commodity_name(self, commodity: dict[str, any]) -> str: + """ + Formats the name of a commodity. + + Args: + commodity (dict[str, any]): The commodity dictionary. + + Returns: + str: The formatted commodity name. + """ + return commodity["name"] + + def get_tools(self) -> list[tuple[str, dict]]: + tools = [ + ( + "get_trading_routes", + { + "type": "function", + "function": { + "name": "get_trading_routes", + "description": "Finds all possible commodity trade options and gives back a selection of the best trade routes. Needs ship name and start position.", + "parameters": { + "type": "object", + "properties": { + "ship_name": {"type": "string"}, + "position_start_name": {"type": "string"}, + "money_to_spend": {"type": "number"}, + "position_end_name": {"type": "string"}, + "free_cargo_space": {"type": "number"}, + "commodity_name": {"type": "string"}, + "illegal_commodities_allowed": {"type": "boolean"}, + "maximal_number_of_routes": {"type": "number"}, + }, + "required": [], + "optional": ( + [ + "ship_name", + "position_start_name", + "money_to_spend", + "free_cargo_space", + "position_end_name", + "commodity_name", + "illegal_commodities_allowed", + "maximal_number_of_routes", + ] + ), + }, + }, + }, + ), + ( + "get_locations_to_sell_to", + { + "type": "function", + "function": { + "name": "get_locations_to_sell_to", + "description": "Finds the best locations at what the player can sell cargo at. Only give position_name if the player specifically wanted to filter for it. Needs commodity name.", + "parameters": { + "type": "object", + "properties": { + "commodity_name": {"type": "string"}, + "ship_name": {"type": "string"}, + "position_name": {"type": "string"}, + "commodity_amount": {"type": "number"}, + "maximal_number_of_locations": {"type": "number"}, + }, + "required": ["commodity_name"], + "optional": [ + "ship_name", + "position_name", + "commodity_amount", + "maximal_number_of_locations", + ], + }, + }, + }, + ), + ( + "get_locations_to_buy_from", + { + "type": "function", + "function": { + "name": "get_locations_to_buy_from", + "description": "Finds the best locations at what the player can buy cargo at. Only give position_name if the player specifically wanted to filter for it. Needs commodity name.", + "parameters": { + "type": "object", + "properties": { + "commodity_name": {"type": "string"}, + "ship_name": {"type": "string"}, + "position_name": {"type": "string"}, + "commodity_amount": {"type": "number"}, + "maximal_number_of_locations": {"type": "number"}, + }, + "required": ["commodity_name"], + "optional": [ + "ship_name", + "position_name", + "commodity_amount", + "maximal_number_of_locations", + ], + }, + }, + }, + ), + ( + "get_location_information", + { + "type": "function", + "function": { + "name": "get_location_information", + "description": "Gives information and commodity prices of this location. Execute this if the player asks for all buy or sell options for a specific location.", + "parameters": { + "type": "object", + "properties": { + "location_name": {"type": "string"}, + }, + "required": ["location_name"], + }, + }, + }, + ), + ( + "get_ship_information", + { + "type": "function", + "function": { + "name": "get_ship_information", + "description": "Gives information about the given ship. If a player asks to rent something or buy a ship, this function needs to be executed.", + "parameters": { + "type": "object", + "properties": { + "ship_name": {"type": "string"}, + }, + "required": ["ship_name"], + }, + }, + }, + ), + ( + "get_ship_comparison", + { + "type": "function", + "function": { + "name": "get_ship_comparison", + "description": "Gives information about given ships. Also execute this function if the player asks for a ship information on multiple ships or a model series.", + "parameters": { + "type": "object", + "properties": { + "ship_names": { + "type": "array", + "items": {"type": "string"}, + }, + }, + "required": ["ship_names"], + }, + }, + }, + ), + ( + "get_commodity_information", + { + "type": "function", + "function": { + "name": "get_commodity_information", + "description": "Gives information about the given commodity. If a player asks for information about a commodity, this function needs to be executed.", + "parameters": { + "type": "object", + "properties": { + "commodity_name": {"type": "string"}, + }, + "required": ["commodity_name"], + }, + }, + }, + ), + ( + "get_commodity_prices_and_tradeports", + { + "type": "function", + "function": { + "name": "get_commodity_prices_and_tradeports", + "description": "Gives information about the given commodity and its buy and sell offers. If a player asks for buy and sell information or locations on a commodity, this function needs to be executed.", + "parameters": { + "type": "object", + "properties": { + "commodity_name": {"type": "string"}, + }, + "required": ["commodity_name"], + }, + }, + }, + ), + ( + "reload_current_commodity_prices", + { + "type": "function", + "function": { + "name": "reload_current_commodity_prices", + "description": "Reloads the current commodity prices from UEX corp.", + "parameters": { + "type": "object", + "properties": {}, + "required": [], + }, + }, + }, + ), + ] + + if self.DEV_MODE: + tools.append( + ( + "show_cached_function_values", + { + "type": "function", + "function": { + "name": "show_cached_function_values", + "description": "Prints the cached function's argument values to the console.", + "parameters": { + "type": "object", + "properties": {}, + "required": [], + }, + }, + }, + ), + ) + + return tools + + async def execute_tool( + self, tool_name: str, parameters: dict[str, any] + ) -> tuple[str, str]: + function_response = "" + instant_response = "" + + functions = { + "get_trading_routes": "get_trading_routes", + "get_locations_to_sell_to": "get_locations_to_sell_to", + "get_locations_to_buy_from": "get_locations_to_buy_from", + "get_location_information": "get_location_information", + "get_ship_information": "get_ship_information", + "get_ship_comparison": "get_ship_comparison", + "get_commodity_information": "get_commodity_information", + "get_commodity_prices_and_tradeports": "get_commodity_information", + "reload_current_commodity_prices": "reload_current_commodity_prices", + "show_cached_function_values": "show_cached_function_values", + } + + try: + if tool_name in functions: + if(not self.skill_loaded): + self.skill_loaded_asked = True + await self._print("UEXcorp skill is not loaded yet. Please wait a moment.", False, False) + function_response = "Data is still beeing loaded. Please wait a moment." + return function_response, instant_response + + self.start_execution_benchmark() + await self._print(f"Executing function: {tool_name}") + function = getattr(self, "_gpt_call_" + functions[tool_name]) + function_response = await function(**parameters) + if self.settings.debug_mode: + await self.print_execution_time() + if self.DEV_MODE: + await self._print(f"_gpt_call_{functions[tool_name]} response: {function_response}", True) + except Exception: + file_object = open(self.logfileerror, "a", encoding="UTF-8") + file_object.write(traceback.format_exc()) + file_object.write( + "========================================================================================\n" + ) + file_object.write( + f"Above error while executing custom function: _gpt_call_{tool_name}\n" + ) + file_object.write(f"With parameters: {parameters}\n") + file_object.write(f"On date: {datetime.now()}\n") + file_object.write(f"Version: {self.skill_version}\n") + file_object.write( + "========================================================================================\n" + ) + file_object.close() + await self._print( + f"Error while executing custom function: {tool_name}\nCheck log file for more details." + ) + function_response = f"Error while executing custom function: {tool_name}" + function_response += "\nTell user there seems to be an error. And you must say that it should be report to the 'uexcorp skill developer (JayMatthew on Discord)'." + + return function_response, instant_response + + async def _find_closest_match( + self, search: str | None, lst: list[str] | set[str] + ) -> str | None: + """ + Finds the closest match to a given string in a list. + Or returns an exact match if found. + If it is not an exact match, OpenAI is used to find the closest match. + + Args: + search (str): The search to find a match for. + lst (list): The list of strings to search for a match. + + Returns: + str or None: The closest match found in the list, or None if no match is found. + """ + if search is None or search == "None": + return None + + self._log(f"Searching for closest match to '{search}' in list.", True) + + checksum = f"{hash(frozenset(lst))}-{hash(search)}" + if checksum in self.cache["search_matches"]: + match = self.cache["search_matches"][checksum] + self._log(f"Found closest match to '{search}' in cache: '{match}'", True) + return match + + if search in lst: + self._log(f"Found exact match to '{search}' in list.", True) + return search + + # make a list of possible matches + closest_matches = difflib.get_close_matches(search, lst, n=10, cutoff=0.4) + closest_matches.extend(item for item in lst if search.lower() in item.lower()) + self._log( + f"Making a list for closest matches for search term '{search}': {', '.join(closest_matches)}", + True, + ) + + if not closest_matches: + self._log( + f"No closest match found for '{search}' in list. Returning None.", True + ) + return None + + messages = [ + { + "role": "system", + "content": f""" + I'll give you just a string value. + You will figure out, what value in this list represents this value best: {', '.join(closest_matches)} + Keep in mind that the given string value can be misspelled or has missing words as it has its origin in a speech to text process. + You must only return the value of the closest match to the given value from the defined list, nothing else. + For example if "Hercules A2" is given and the list contains of "A2, C2, M2", you will return "A2" as string. + Or if "C2" is given and the list contains of "A2 Hercules Star Lifter, C2 Monster Truck, M2 Extreme cool ship", you will return "C2 Monster Truck" as string. + On longer search terms, prefer the exact match, if it is in the list. + The response must not contain anything else, than the exact value of the closest match from the list. + If you can't find a match, return 'None'. Do never return the given search value. + """, + }, + { + "role": "user", + "content": search, + }, + ] + completion = await self.gpt_call(messages) + answer = ( + completion.choices[0].message.content + if completion and completion.choices + else "" + ) + + if not answer: + dumb_match = difflib.get_close_matches( + search, closest_matches, n=1, cutoff=0.9 + ) + if dumb_match: + self._log( + f"OpenAI did not answer for '{search}'. Returning dumb match '{dumb_match}'", + True, + ) + return dumb_match[0] + else: + self._log( + f"OpenAI did not answer for '{search}' and dumb match not possible. Returning None.", + True, + ) + return None + + self._log(f"OpenAI answered: '{answer}'", True) + + if answer == "None" or answer not in closest_matches: + self._log( + f"No closest match found for '{search}' in list. Returning None.", True + ) + return None + + self._log(f"Found closest match to '{search}' in list: '{answer}'", True) + self._add_context(f"\n\nInstead of '{search}', you should use '{answer}'.") + self.cache["search_matches"][checksum] = answer + return answer + + async def get_prompt(self) -> str | None: + """Return additional context.""" + additional_context = self.config.prompt or "" + additional_context += "\n" + self.dynamic_context + return additional_context + + async def _gpt_call_show_cached_function_values(self) -> str: + """ + Prints the cached function's argument values to the console. + + Returns: + str: A message indicating that the cached function's argument values have been printed to the console. + """ + self._log(self.cache["function_args"], True) + return "The cached function values are: \n" + json.dumps(self.cache["function_args"]) + + async def _gpt_call_reload_current_commodity_prices(self) -> str: + """ + Reloads the current commodity prices from UEX corp. + + Returns: + str: A message indicating that the current commodity prices have been reloaded. + """ + await self._load_data(True) + # clear cached data + for key in self.cache: + self.cache[key] = {} + + self._log("Reloaded current commodity prices from UEX corp.", True) + return "Reloaded current commodity prices from UEX corp." + + async def _gpt_call_get_commodity_information( + self, commodity_name: str = None + ) -> str: + """ + Retrieves information about a given commodity. + + Args: + commodity_name (str, optional): The name of the commodity. Defaults to None. + + Returns: + str: The information about the commodity in JSON format, or an error message if the commodity is not found. + """ + self._log(f"Parameters: Commodity: {commodity_name}", True) + + commodity_name = self._get_function_arg_from_cache( + "commodity_name", commodity_name + ) + + if commodity_name is None: + self._log("No commodity given. Ask for a commodity.", True) + return "No commodity given. Ask for a commodity." + + misunderstood = [] + closest_match = await self._find_closest_match( + commodity_name, self.commodity_names + ) + if closest_match is None: + misunderstood.append(f"Commodity: {commodity_name}") + else: + commodity_name = closest_match + + self._log(f"Interpreted Parameters: Commodity: {commodity_name}", True) + + if misunderstood: + misunderstood_str = ", ".join(misunderstood) + self._log( + f"These given parameters do not exist in game. Exactly ask for clarification of these values: {misunderstood_str}", + True, + ) + return f"These given parameters do not exist in game. Exactly ask for clarification of these values: {misunderstood_str}" + + commodity = self._get_commodity_by_name(commodity_name) + if commodity is not None: + output_commodity = self._get_converted_commodity_for_output(commodity) + self._log(output_commodity, True) + return json.dumps(output_commodity) + + async def _gpt_call_get_ship_information(self, ship_name: str = None) -> str: + """ + Retrieves information about a specific ship. + + Args: + ship_name (str, optional): The name of the ship. Defaults to None. + + Returns: + str: The ship information or an error message. + + """ + self._log(f"Parameters: Ship: {ship_name}", True) + + ship_name = self._get_function_arg_from_cache("ship_name", ship_name) + + if ship_name is None: + self._log("No ship given. Ask for a ship. Dont say sorry.", True) + return "No ship given. Ask for a ship. Dont say sorry." + + misunderstood = [] + closest_match = await self._find_closest_match(ship_name, self.ship_names) + if closest_match is None: + misunderstood.append(f"Ship: {ship_name}") + else: + ship_name = closest_match + + self._log(f"Interpreted Parameters: Ship: {ship_name}", True) + + if misunderstood: + misunderstood_str = ", ".join(misunderstood) + self._log( + f"These given parameters do not exist in game. Exactly ask for clarification of these values: {misunderstood_str}", + True, + ) + return f"These given parameters do not exist in game. Exactly ask for clarification of these values: {misunderstood_str}" + + ship = self._get_ship_by_name(ship_name) + if ship is not None: + output_ship = self._get_converted_ship_for_output(ship) + self._log(output_ship, True) + return json.dumps(output_ship) + + async def _gpt_call_get_ship_comparison(self, ship_names: list[str] = None) -> str: + """ + Retrieves information about multiple ships. + + Args: + ship_names (list[str], optional): The names of the ships. Defaults to None. + + Returns: + str: The ship information or an error message. + """ + self._log(f"Parameters: Ships: {', '.join(ship_names)}", True) + + if ship_names is None or not ship_names: + self._log("No ship given. Ask for a ship. Dont say sorry.", True) + return "No ship given. Ask for a ship. Dont say sorry." + + misunderstood = [] + ships = [] + for ship_name in ship_names: + closest_match = await self._find_closest_match(ship_name, self.ship_names) + if closest_match is None: + misunderstood.append(ship_name) + else: + ship_name = closest_match + ships.append(self._get_ship_by_name(ship_name)) + + self._log(f"Interpreted Parameters: Ships: {', '.join(ship_names)}", True) + + if misunderstood: + self._log( + f"These ship names do not exist in game. Exactly ask for clarification of these ships: {', '.join(misunderstood)}", + True, + ) + return f"These ship names do not exist in game. Exactly ask for clarification of these ships: {', '.join(misunderstood)}" + + output = {} + for ship in ships: + output[self._format_ship_name(ship)] = self._get_converted_ship_for_output( + ship + ) + + output = ( + "Point out differences between these ships but keep it short, like 4-5 sentences, and dont mention something both cant do:\n" + + json.dumps(output) + ) + self._log(output, True) + return output + + async def _gpt_call_get_location_information( + self, location_name: str = None + ) -> str: + """ + Retrieves information about a given location. + + Args: + location_name (str, optional): The name of the location. Defaults to None. + + Returns: + str: The information about the location in JSON format, or an error message if the location is not found. + """ + self._log(f"Parameters: Location: {location_name}", True) + + location_name = self._get_function_arg_from_cache( + "location_name", location_name + ) + + if location_name is None: + self._log("No location given. Ask for a location.", True) + return "No location given. Ask for a location." + + misunderstood = [] + closest_match = await self._find_closest_match( + location_name, self.location_names_set + ) + if closest_match is None: + misunderstood.append(f"Location: {location_name}") + else: + location_name = closest_match + + self._log(f"Interpreted Parameters: Location: {location_name}", True) + + if misunderstood: + misunderstood_str = ", ".join(misunderstood) + self._log( + f"These given parameters do not exist in game. Exactly ask for clarification of these values: {misunderstood_str}", + True, + ) + return f"These given parameters do not exist in game. Exactly ask for clarification of these values: {misunderstood_str}" + + # get a clone of the data + tradeport = self._get_tradeport_by_name(location_name) + if tradeport is not None: + output = self._get_converted_tradeport_for_output(tradeport) + self._log(output, True) + return json.dumps(output) + city = self._get_city_by_name(location_name) + if city is not None: + output = self._get_converted_city_for_output(city) + self._log(output, True) + return json.dumps(output) + satellite = self._get_satellite_by_name(location_name) + if satellite is not None: + output = self._get_converted_satellite_for_output(satellite) + self._log(output, True) + return json.dumps(output) + planet = self._get_planet_by_name(location_name) + if planet is not None: + output = self._get_converted_planet_for_output(planet) + self._log(output, True) + return json.dumps(output) + system = self._get_system_by_name(location_name) + if system is not None: + output = self._get_converted_system_for_output(system) + self._log(output, True) + return json.dumps(output) + + def _get_converted_tradeport_for_output( + self, tradeport: dict[str, any] + ) -> dict[str, any]: + """ + Converts a tradeport dictionary to a dictionary that can be used as output. + + Args: + tradeport (dict[str, any]): The tradeport dictionary to be converted. + + Returns: + dict[str, any]: The converted tradeport dictionary. + """ + checksum = f"tradeport--{tradeport['id']}" + if checksum in self.cache["readable_objects"]: + return self.cache["readable_objects"][checksum] + + output = { + "type": "Tradeport", + "name": self._format_tradeport_name(tradeport), + "star_system": self._get_system_name_by_code(tradeport["id_star_system"]), + "planet": self._get_planet_name_by_code(tradeport["id_planet"]), + "city": self._get_city_name_by_code(tradeport["id_city"]), + "satellite": self._get_satellite_name_by_code(tradeport["id_moon"]), + } + output["hull_trading"] = ( + "Trading with large ships, that need a loading area, is possible." + if tradeport["hull_trading"] + else "Trading with large ships, that need a loading area, is not possible." + ) + + buyable_commodities = [ + f"{data['name']} for {data['price_buy']} aUEC per SCU" + for commodity_code, data in tradeport["prices"].items() + if data["operation"] == "buy" + ] + sellable_commodities = [ + f"{data['name']} for {data['price_sell']} aUEC per SCU" + for commodity_code, data in tradeport["prices"].items() + if data["operation"] == "sell" + ] + + if len(buyable_commodities): + output["buyable_commodities"] = ", ".join(buyable_commodities) + if len(sellable_commodities): + output["sellable_commodities"] = ", ".join(sellable_commodities) + + for key in ["system", "planet", "city", "satellite"]: + if output.get(key) is None: + output.pop(key, None) + + self.cache["readable_objects"][checksum] = output + return output + + def _get_converted_city_for_output(self, city: dict[str, any]) -> dict[str, any]: + """ + Converts a city dictionary to a dictionary that can be used as output. + + Args: + city (dict[str, any]): The city dictionary to be converted. + + Returns: + dict[str, any]: The converted city dictionary. + """ + checksum = f"city--{city['id']}" + if checksum in self.cache["readable_objects"]: + return self.cache["readable_objects"][checksum] + + output = { + "type": "City", + "name": self._format_city_name(city), + "star_system": self._get_system_name_by_code(city["id_star_system"]), + "planet": self._get_planet_name_by_code(city["id_planet"]), + "moon": self._get_satellite_name_by_code(city["id_moon"]), + "is_armistice": "Yes" if city["is_armistice"] else "No", + "has_freight_elevator": "Yes" if city["has_freight_elevator"] else "No", + "has_docking_ports": "Yes" if city["has_docking_port"] else "No", + "has_clinic": "Yes" if city["has_clinic"] else "No", + "has_food": "Yes" if city["has_food"] else "No", + "has_refuel_option": "Yes" if city["has_refuel"] else "No", + "has_repair_option": "Yes" if city["has_repair"] else "No", + "has_refinery": "Yes" if city["has_refinery"] else "No", + } + + tradeports = self._get_tradeports_by_position_name(city["name"]) + if tradeports: + output["options_to_trade"] = ", ".join( + [self._format_tradeport_name(tradeport) for tradeport in tradeports] + ) + + for key in ["star_system", "planet", "moon"]: + if output.get(key) is None: + output.pop(key, None) + + self.cache["readable_objects"][checksum] = output + return output + + def _get_converted_satellite_for_output( + self, satellite: dict[str, any] + ) -> dict[str, any]: + """ + Converts a satellite dictionary to a dictionary that can be used as output. + + Args: + satellite (dict[str, any]): The satellite dictionary to be converted. + + Returns: + dict[str, any]: The converted satellite dictionary. + """ + checksum = f"satellite--{satellite['id']}" + if checksum in self.cache["readable_objects"]: + return self.cache["readable_objects"][checksum] + + output = { + "type": "Moon", + "name": self._format_satellite_name(satellite), + "star_system": self._get_system_name_by_code(satellite["id_star_system"]), + "orbits_planet": self._get_planet_name_by_code(satellite["id_planet"]), + } + + tradeports = self._get_tradeports_by_position_name(self._format_satellite_name(satellite)) + if tradeports: + output["options_to_trade"] = ", ".join( + [self._format_tradeport_name(tradeport) for tradeport in tradeports] + ) + + for key in ["star_system", "orbits_planet"]: + if output.get(key) is None: + output.pop(key, None) + + self.cache["readable_objects"][checksum] = output + return output + + def _get_converted_planet_for_output( + self, planet: dict[str, any] + ) -> dict[str, any]: + """ + Converts a planet dictionary to a dictionary that can be used as output. + + Args: + planet (dict[str, any]): The planet dictionary to be converted. + + Returns: + dict[str, any]: The converted planet dictionary. + """ + checksum = f"planet--{planet['id']}" + if checksum in self.cache["readable_objects"]: + return self.cache["readable_objects"][checksum] + + output = { + "type": "Planet", + "name": self._format_planet_name(planet), + "star_system": self._get_system_name_by_code(planet["id_star_system"]), + } + + tradeports = self._get_tradeports_by_position_name(planet["name"]) + if tradeports: + output["options_to_trade"] = ", ".join( + [self._format_tradeport_name(tradeport) for tradeport in tradeports] + ) + + satellites = self._get_satellites_by_planetcode(planet["code"]) + if satellites: + output["satellites"] = ", ".join( + [self._format_satellite_name(satellite) for satellite in satellites] + ) + + cities = self._get_cities_by_planetcode(planet["code"]) + if cities: + output["cities"] = ", ".join( + [self._format_city_name(city) for city in cities] + ) + + for key in ["star_system"]: + if output.get(key) is None: + output.pop(key, None) + + self.cache["readable_objects"][checksum] = output + return output + + def _get_converted_system_for_output( + self, system: dict[str, any] + ) -> dict[str, any]: + """ + Converts a system dictionary to a dictionary that can be used as output. + + Args: + system (dict[str, any]): The system dictionary to be converted. + + Returns: + dict[str, any]: The converted system dictionary. + """ + checksum = f"system--{system['id']}" + if checksum in self.cache["readable_objects"]: + return self.cache["readable_objects"][checksum] + + output = { + "type": "Star System", + "name": self._format_system_name(system), + } + + tradeports = self._get_tradeports_by_position_name(system["name"]) + if tradeports: + output["options_to_trade"] = f"{len(tradeports)} different options to trade." + tradeport_without_planets = [] + gateways = [] + for tradeport in tradeports: + if not tradeport["id_planet"]: + if tradeport["name"].find("Gateway") != -1: + gateways.append(tradeport) + else: + tradeport_without_planets.append(tradeport) + if tradeport_without_planets: + output["space_stations"] = ", ".join( + [self._format_tradeport_name(tradeport) for tradeport in tradeport_without_planets] + ) + if gateways: + output["gateways"] = ", ".join( + [self._format_tradeport_name(tradeport) for tradeport in gateways] + ) + + planets = self._get_planets_by_systemcode(system["code"]) + if planets: + output["planets"] = ", ".join( + [self._format_planet_name(planet) for planet in planets] + ) + + self.cache["readable_objects"][checksum] = output + return output + + def _get_converted_ship_for_output(self, ship: dict[str, any]) -> dict[str, any]: + """ + Converts a ship dictionary to a dictionary that can be used as output. + + Args: + ship (dict[str, any]): The ship dictionary to be converted. + + Returns: + dict[str, any]: The converted ship dictionary. + """ + checksum = f"ship--{ship['id']}" + if checksum in self.cache["readable_objects"]: + return self.cache["readable_objects"][checksum] + + output = { + "type": "Ship" if ship["is_spaceship"] else "Groud Vehicle", + "name": self._format_ship_name(ship), + "manufacturer": ship["company_name"], + "cargo_capacity": f"{ship['scu']} SCU", + "added_on_version": "Unknown" if ship["is_concept"] else ship["game_version"], + "field_of_activity": self._get_ship_field_of_activity(ship), + } + + buy_rent_options = self._get_converted_rent_and_buy_option_for_output(ship) + if "buy_at" in buy_rent_options: + output["buy_at"] = buy_rent_options["buy_at"] + + if "rent_at" in buy_rent_options: + output["rent_at"] = buy_rent_options["rent_at"] + + if ship["hull_trading"] is True: + output["trading_info"] = ( + "This ship can only trade on suitable space stations with cargo loading option." + ) + + self.cache["readable_objects"][checksum] = output + return output + + def _get_ship_field_of_activity(self, ship: dict[str, any]) -> str: + """ + Returns the field of activity of a ship. + + Args: + ship (dict[str, any]): The ship dictionary to get the field of activity for. + + Returns: + str: The field of activity of the ship. + """ + + field = [] + if ship["is_exploration"]: + field.append("Exploration") + if ship["is_mining"]: + field.append("Mining") + if ship["is_salvage"]: + field.append("Salvage") + if ship["is_refinery"]: + field.append("Refinery") + if ship["is_scanning"]: + field.append("Scanning") + if ship["is_cargo"]: + field.append("Cargo") + if ship["is_medical"]: + field.append("Medical") + if ship["is_racing"]: + field.append("Racing") + if ship["is_repair"]: + field.append("Repair") + if ship["is_refuel"]: + field.append("Refuel") + if ship["is_interdiction"]: + field.append("Interdiction") + if ship["is_tractor_beam"]: + field.append("Tractor Beam") + if ship["is_qed"]: + field.append("Quantum Interdiction") + if ship["is_emp"]: + field.append("EMP") + if ship["is_construction"]: + field.append("Construction") + if ship["is_datarunner"]: + field.append("Datarunner") + if ship["is_science"]: + field.append("Science") + if ship["is_boarding"]: + field.append("Boarding") + if ship["is_stealth"]: + field.append("Stealth") + if ship["is_research"]: + field.append("Research") + if ship["is_carrier"]: + field.append("Carrier") + + addition = [] + if ship["is_civilian"]: + addition.append("Civilian") + if ship["is_military"]: + addition.append("Military") + + return f"{', '.join(field)} ({' & '.join(addition)})" + + def _get_converted_rent_and_buy_option_for_output( + self, ship: dict[str, any] + ) -> dict[str, any]: + """ + Converts the rent and buy options of a ship to a dictionary that can be used as output. + + Args: + ship (dict[str, any]): The ship dictionary to get the rent and buy options for. + + Returns: + dict[str, any]: The converted rent and buy options dictionary. + """ + + # TODO: implement this with API v2 + return {} + + def _get_converted_commodity_for_output( + self, commodity: dict[str, any] + ) -> dict[str, any]: + """ + Converts a commodity dictionary to a dictionary that can be used as output. + + Args: + commodity (dict[str, any]): The commodity dictionary to be converted. + + Returns: + dict[str, any]: The converted commodity dictionary. + """ + checksum = f"commodity--{commodity['id']}" + if checksum in self.cache["readable_objects"]: + return self.cache["readable_objects"][checksum] + + output = { + "type": "Commodity", + "subtype": commodity["kind"], + "name": commodity["name"], + } + + price_buy_best = None + price_sell_best = None + output["buy_at"] = {} + output["sell_at"] = {} + + for tradeport in self.tradeports: + if "prices" not in tradeport: + continue + if commodity["id"] in tradeport["prices"]: + if tradeport["prices"][commodity["id"]]["operation"] == "buy": + price_buy = tradeport["prices"][commodity["id"]]["price_buy"] + if price_buy_best is None or price_buy < price_buy_best: + price_buy_best = price_buy + output["buy_at"][ + self._format_tradeport_name(tradeport) + ] = f"{price_buy} aUEC" + else: + price_sell = tradeport["prices"][commodity["id"]]["price_sell"] + if price_sell_best is None or price_sell > price_sell_best: + price_sell_best = price_sell + output["sell_at"][ + self._format_tradeport_name(tradeport) + ] = f"{price_sell} aUEC" + + output["best_buy_price"] = ( + f"{price_buy_best} aUEC" if price_buy_best else "Not buyable." + ) + output["best_sell_price"] = ( + f"{price_sell_best} aUEC" if price_sell_best else "Not sellable." + ) + + boolean_keys = ["is_harvestable", "is_mineral", "is_illegal"] + for key in boolean_keys: + output[key] = "Yes" if commodity[key] else "No" + + if commodity["is_illegal"]: + output["notes"] = "Stay away from ship scanns to avoid fines and crimestat, as this commodity is illegal." + + self.cache["readable_objects"][checksum] = output + return output + + async def _gpt_call_get_locations_to_sell_to( + self, + commodity_name: str = None, + ship_name: str = None, + position_name: str = None, + commodity_amount: int = 1, + maximal_number_of_locations: int = 5, + ) -> str: + await self._print( + f"Given Parameters: Commodity: {commodity_name}, Ship Name: {ship_name}, Current Position: {position_name}, Amount: {commodity_amount}, Maximal Number of Locations: {maximal_number_of_locations}", + True, + ) + + commodity_name = self._get_function_arg_from_cache( + "commodity_name", commodity_name + ) + ship_name = self._get_function_arg_from_cache("ship_name", ship_name) + + if commodity_name is None: + self._log("No commodity given. Ask for a commodity.", True) + return "No commodity given. Ask for a commodity." + + misunderstood = [] + parameters = { + "commodity_name": (commodity_name, self.commodity_names), + "ship_name": (ship_name, self.ship_names), + "position_name": (position_name, self.location_names_set), + } + for param, (value, names_set) in parameters.items(): + if value is not None: + match = await self._find_closest_match(value, names_set) + if match is None: + misunderstood.append(f"{param}: {value}") + else: + self._set_function_arg_to_cache(param, match) + parameters[param] = (match, names_set) + commodity_name = parameters["commodity_name"][0] + ship_name = parameters["ship_name"][0] + position_name = parameters["position_name"][0] + + await self._print( + f"Interpreted Parameters: Commodity: {commodity_name}, Ship Name: {ship_name}, Position: {position_name}, Amount: {commodity_amount}, Maximal Number of Locations: {maximal_number_of_locations}", + True, + ) + + if misunderstood: + self._log( + "These given parameters do not exist in game. Exactly ask for clarification of these values: " + + ", ".join(misunderstood), + True, + ) + return ( + "These given parameters do not exist in game. Exactly ask for clarification of these values: " + + ", ".join(misunderstood) + ) + + tradeports = ( + self.tradeports + if position_name is None + else self._get_tradeports_by_position_name(position_name) + ) + commodity = self._get_commodity_by_name(commodity_name) + ship = self._get_ship_by_name(ship_name) + amount = max(1, int(commodity_amount or 1)) + maximal_number_of_locations = max(1, int(maximal_number_of_locations or 3)) + + selloptions = collections.defaultdict(list) + for tradeport in tradeports: + sellprice = self._get_data_location_sellprice( + tradeport, commodity, ship, amount + ) + if sellprice is not None: + selloptions[sellprice].append(tradeport) + + selloptions = dict(sorted(selloptions.items(), reverse=True)) + selloptions = dict( + itertools.islice(selloptions.items(), maximal_number_of_locations) + ) + + messages = [ + f"Here are the best {len(selloptions)} locations to sell {amount} SCU {commodity_name}:" + ] + + for sellprice, tradeports in selloptions.items(): + messages.append(f"{sellprice} aUEC:") + messages.extend( + self._get_tradeport_route_description(tradeport) + for tradeport in tradeports + ) + messages.append("\n") + + self._log("\n".join(messages), True) + return "\n".join(messages) + + async def _gpt_call_get_locations_to_buy_from( + self, + commodity_name: str = None, + ship_name: str = None, + position_name: str = None, + commodity_amount: int = 1, + maximal_number_of_locations: int = 5, + ) -> str: + await self._print( + f"Given Parameters: Commodity: {commodity_name}, Ship Name: {ship_name}, Current Position: {position_name}, Amount: {commodity_amount}, Maximal Number of Locations: {maximal_number_of_locations}", + True, + ) + + commodity_name = self._get_function_arg_from_cache( + "commodity_name", commodity_name + ) + ship_name = self._get_function_arg_from_cache("ship_name", ship_name) + + if commodity_name is None: + self._log("No commodity given. Ask for a commodity.", True) + return "No commodity given. Ask for a commodity." + + misunderstood = [] + parameters = { + "ship_name": (ship_name, self.ship_names), + "location_name": (position_name, self.location_names_set), + "commodity_name": (commodity_name, self.commodity_names), + } + for param, (value, names_set) in parameters.items(): + if value is not None: + match = await self._find_closest_match(value, names_set) + if match is None: + misunderstood.append(f"{param}: {value}") + else: + self._set_function_arg_to_cache(param, match) + parameters[param] = (match, names_set) + ship_name = parameters["ship_name"][0] + position_name = parameters["location_name"][0] + commodity_name = parameters["commodity_name"][0] + + await self._print( + f"Interpreted Parameters: Commodity: {commodity_name}, Ship Name: {ship_name}, Position: {position_name}, Amount: {commodity_amount}, Maximal Number of Locations: {maximal_number_of_locations}", + True, + ) + + if misunderstood: + self._log( + "These given parameters do not exist in game. Exactly ask for clarification of these values: " + + ", ".join(misunderstood), + True, + ) + return ( + "These given parameters do not exist in game. Exactly ask for clarification of these values: " + + ", ".join(misunderstood) + ) + + tradeports = ( + self.tradeports + if position_name is None + else self._get_tradeports_by_position_name(position_name) + ) + commodity = self._get_commodity_by_name(commodity_name) + ship = self._get_ship_by_name(ship_name) + amount = max(1, int(commodity_amount or 1)) + maximal_number_of_locations = max(1, int(maximal_number_of_locations or 3)) + + buyoptions = collections.defaultdict(list) + for tradeport in tradeports: + buyprice = self._get_data_location_buyprice( + tradeport, commodity, ship, amount + ) + if buyprice is not None: + buyoptions[buyprice].append(tradeport) + + buyoptions = dict(sorted(buyoptions.items(), reverse=False)) + buyoptions = dict( + itertools.islice(buyoptions.items(), maximal_number_of_locations) + ) + + messages = [ + f"Here are the best {len(buyoptions)} locations to buy {amount} SCU {commodity_name}:" + ] + for buyprice, tradeports in buyoptions.items(): + messages.append(f"{buyprice} aUEC:") + messages.extend( + self._get_tradeport_route_description(tradeport) + for tradeport in tradeports + ) + messages.append("\n") + + self._log("\n".join(messages), True) + return "\n".join(messages) + + def _get_data_location_sellprice(self, tradeport, commodity, ship=None, amount=1): + if ( + ship is not None + and ship["hull_trading"] is True + and tradeport["hull_trading"] is False + ): + return None + + if "prices" not in tradeport: + return None + + commodity_code = commodity["id"] + for code, price in tradeport["prices"].items(): + if code == commodity_code and price["operation"] == "sell": + return price["price_sell"] * amount + return None + + def _get_data_location_buyprice(self, tradeport, commodity, ship=None, amount=1): + if ( + ship is not None + and ship["hull_trading"] is True + and tradeport["hull_trading"] is False + ): + return None + + if "prices" not in tradeport: + return None + + commodity_code = commodity["id"] + for code, price in tradeport["prices"].items(): + if code == commodity_code and price["operation"] == "buy": + return price["price_buy"] * amount + return None + + async def _gpt_call_get_trading_routes( + self, + ship_name: str = None, + money_to_spend: float = None, + position_start_name: str = None, + free_cargo_space: float = None, + position_end_name: str = None, + commodity_name: str = None, + illegal_commodities_allowed: bool = None, + maximal_number_of_routes: int = None, + ) -> str: + """ + Finds multiple best trading routes based on the given parameters. + + Args: + ship_name (str, optional): The name of the ship. Defaults to None. + money_to_spend (float, optional): The amount of money to spend. Defaults to None. + position_start_name (str, optional): The name of the starting position. Defaults to None. + free_cargo_space (float, optional): The amount of free cargo space. Defaults to None. + position_end_name (str, optional): The name of the ending position. Defaults to None. + commodity_name (str, optional): The name of the commodity. Defaults to None. + illegal_commodities_allowed (bool, optional): Flag indicating whether illegal commodities are allowed. Defaults to True. + maximal_number_of_routes (int, optional): The maximum number of routes to return. Defaults to 2. + + Returns: + str: A string representation of the trading routes found. + """ + + # For later use in distance calculation: + # https://starmap.tk/api/v2/oc/ + # https://starmap.tk/api/v2/pois/ + + await self._print( + f"Parameters: Ship: {ship_name}, Position Start: {position_start_name}, Position End: {position_end_name}, Commodity Name: {commodity_name}, Money: {money_to_spend} aUEC, free_cargo_space: {free_cargo_space} SCU, Maximal Number of Routes: {maximal_number_of_routes}, Illegal Allowed: {illegal_commodities_allowed}", + True, + ) + + ship_name = self._get_function_arg_from_cache("ship_name", ship_name) + illegal_commodities_allowed = self._get_function_arg_from_cache( + "illegal_commodities_allowed", illegal_commodities_allowed + ) + if illegal_commodities_allowed is None: + illegal_commodities_allowed = True + + missing_args = [] + if ship_name is None: + missing_args.append("ship_name") + + if self.uexcorp_tradestart_mandatory and position_start_name is None: + missing_args.append("position_start_name") + + money_to_spend = ( + None + if money_to_spend is not None and int(money_to_spend) < 1 + else money_to_spend + ) + free_cargo_space = ( + None + if free_cargo_space is not None and int(free_cargo_space) < 1 + else free_cargo_space + ) + + misunderstood = [] + parameters = { + "ship_name": (ship_name, self.ship_names), + "position_start_name": (position_start_name, self.location_names_set), + "position_end_name": (position_end_name, self.location_names_set), + "commodity_name": (commodity_name, self.commodity_names), + } + for param, (value, names_set) in parameters.items(): + if value is not None: + match = await self._find_closest_match(value, names_set) + if match is None: + misunderstood.append(f"{param}: {value}") + else: + self._set_function_arg_to_cache(param, match) + parameters[param] = (match, names_set) + ship_name = parameters["ship_name"][0] + position_start_name = parameters["position_start_name"][0] + position_end_name = parameters["position_end_name"][0] + commodity_name = parameters["commodity_name"][0] + + if money_to_spend is not None: + self._set_function_arg_to_cache("money", money_to_spend) + + await self._print( + f"Interpreted Parameters: Ship: {ship_name}, Position Start: {position_start_name}, Position End: {position_end_name}, Commodity Name: {commodity_name}, Money: {money_to_spend} aUEC, free_cargo_space: {free_cargo_space} SCU, Maximal Number of Routes: {maximal_number_of_routes}, Illegal Allowed: {illegal_commodities_allowed}", + True, + ) + + self._set_function_arg_to_cache("money", money_to_spend) + + if misunderstood or missing_args: + misunderstood_str = ", ".join(misunderstood) + missing_str = ", ".join(missing_args) + answer = "" + if missing_str: + answer += f"Missing parameters: {missing_str}. " + if misunderstood_str: + answer += ( + f"These given parameters were misunderstood: {misunderstood_str}" + ) + return answer + + # set variables + ship = self._get_ship_by_name(ship_name) + if money_to_spend is not None: + money = int(money_to_spend) + else: + money = None + if free_cargo_space is not None: + free_cargo_space = int(free_cargo_space) + else: + free_cargo_space = None + commodity = ( + self._get_commodity_by_name(commodity_name) if commodity_name else None + ) + maximal_number_of_routes = int( + maximal_number_of_routes or self.uexcorp_default_trade_route_count + ) + start_tradeports = ( + self._get_tradeports_by_position_name(position_start_name) + if position_start_name + else self.tradeports + ) + end_tradeports = ( + self._get_tradeports_by_position_name(position_end_name) + if position_end_name + else self.tradeports + ) + + commodities = [] + if commodity is None: + commodities = self.commodities + else: + commodities.append(commodity) + + trading_routes = [] + errors = [] + for commodity in commodities: + commodity_routes = [] + if not illegal_commodities_allowed and commodity["is_illegal"]: + continue + for start_tradeport in start_tradeports: + if ( + "prices" not in start_tradeport + or commodity["id"] not in start_tradeport["prices"] + or start_tradeport["prices"][commodity["id"]]["operation"] + != "buy" + ): + continue + for end_tradeport in end_tradeports: + if ( + "prices" not in end_tradeport + or commodity["id"] not in end_tradeport["prices"] + or end_tradeport["prices"][commodity["id"]]["operation"] + != "sell" + ): + continue + + if ( + ship + and ship["hull_trading"] is True + and ( + "hull_trading" not in start_tradeport + or start_tradeport["hull_trading"] is not True + or "hull_trading" not in end_tradeport + or end_tradeport["hull_trading"] is not True + ) + ): + continue + + trading_route_new = self._get_trading_route( + ship, + start_tradeport, + end_tradeport, + money, + free_cargo_space, + commodity, + illegal_commodities_allowed, + ) + + if isinstance(trading_route_new, str): + if trading_route_new not in errors: + errors.append(trading_route_new) + else: + commodity_routes.append(trading_route_new) + + if len(commodity_routes) > 0: + if self.uexcorp_summarize_routes_by_commodity: + best_commodity_routes = heapq.nlargest( + 1, commodity_routes, key=lambda k: int(k["profit"]) + ) + trading_routes.extend(best_commodity_routes) + else: + trading_routes.extend(commodity_routes) + + if len(trading_routes) > 0: + additional_answer = "" + if len(trading_routes) < maximal_number_of_routes: + additional_answer += f" There are only {len(trading_routes)} with different commodities available. " + else: + additional_answer += f" There are {len(trading_routes)} routes available and these are the best {maximal_number_of_routes} ones." + + # sort trading routes by profit and limit to maximal_number_of_routes + trading_routes = heapq.nlargest( + maximal_number_of_routes, trading_routes, key=lambda k: int(k["profit"]) + ) + + for trading_route in trading_routes: + destinationselection = [] + for tradeport in trading_route["end"]: + destinationselection.append( + f"{self._get_tradeport_route_description(tradeport)}" + ) + trading_route["end"] = " OR ".join(destinationselection) + startselection = [] + for tradeport in trading_route["start"]: + startselection.append( + f"{self._get_tradeport_route_description(tradeport)}" + ) + trading_route["start"] = " OR ".join(startselection) + + # format the trading routes + for trading_route in trading_routes: + trading_route["start"] = trading_route["start"] + trading_route["end"] = trading_route["end"] + trading_route["commodity"] = self._format_commodity_name( + trading_route["commodity"] + ) + trading_route["profit"] = f"{trading_route['profit']} aUEC" + trading_route["buy"] = f"{trading_route['buy']} aUEC" + trading_route["sell"] = f"{trading_route['sell']} aUEC" + trading_route["cargo"] = f"{trading_route['cargo']} SCU" + + message = ( + "Possible commodities with their profit. Just give basic overview at first.\n" + + additional_answer + + " JSON: \n " + + json.dumps(trading_routes) + ) + return message + else: + return_string = "No trading routes found." + if len(errors) > 0: + return_string += "\nPossible errors are:\n- " + "\n- ".join(errors) + return return_string + + def _get_trading_route( + self, + ship: dict[str, any], + position_start: dict[str, any], + position_end: dict[str, any], + money: int = None, + free_cargo_space: int = None, + commodity: dict[str, any] = None, + illegal_commodities_allowed: bool = True, + ) -> str: + """ + Finds the best trading route based on the given parameters. + + Args: + ship (dict[str, any]): The ship dictionary. + position_start (dict[str, any]): The starting position dictionary. + money (int, optional): The amount of money to spend. Defaults to None. + free_cargo_space (int, optional): The amount of free cargo space. Defaults to None. + position_end (dict[str, any], optional): The ending position dictionary. Defaults to None. + commodity (dict[str, any], optional): The commodity dictionary. Defaults to None. + illegal_commodities_allowed (bool, optional): Flag indicating whether illegal commodities are allowed. Defaults to True. + + Returns: + str: A string representation of the trading route found. JSON if the route is found, otherwise an error message. + """ + + # set variables + cargo_space = ship["scu"] + if free_cargo_space: + cargo_space = free_cargo_space + if free_cargo_space > ship["scu"]: + cargo_space = ship["scu"] + + if cargo_space < 1: + return "Your ship has no cargo space to trade." + + commodity_filter = commodity + start_tradeports = [position_start] + if ship["hull_trading"] is True: + start_tradeports = [ + tradeport + for tradeport in start_tradeports + if "hull_trading" in tradeport and tradeport["hull_trading"] is True + ] + if len(start_tradeports) < 1: + if ship["hull_trading"] is True: + return "No valid start position given. Make sure to provide a start point compatible with your ship." + return "No valid start position given. Try a different position or just name a planet or star system." + + end_tradeports = [position_end] + if ship["hull_trading"] is True: + end_tradeports = [ + tradeport + for tradeport in end_tradeports + if "hull_trading" in tradeport and tradeport["hull_trading"] is True + ] + if len(end_tradeports) < 1: + return "No valid end position given." + + if ( + len(end_tradeports) == 1 + and len(start_tradeports) == 1 + and end_tradeports[0]["id"] == start_tradeports[0]["id"] + ): + return "Start and end position are the same." + + if money is not None and money <= 0: + return "You dont have enough money to trade." + + best_route = { + "start": [], + "end": [], + "commodity": {}, + "profit": 0, + "cargo": 0, + "buy": 0, + "sell": 0, + } + + # apply trade port blacklist + if self.uexcorp_trade_blacklist: + for blacklist_item in self.uexcorp_trade_blacklist: + if "tradeport" in blacklist_item and blacklist_item["tradeport"]: + for tradeport in start_tradeports: + if self._format_tradeport_name(tradeport) == blacklist_item["tradeport"]: + if ( + "commodity" not in blacklist_item + or not blacklist_item["commodity"] + ): + # remove tradeport, if no commodity given + start_tradeports.remove(tradeport) + break + else: + commodity = self._get_commodity_by_name( + blacklist_item["commodity"] + ) + for commodity_code, data in tradeport["prices"].items(): + if commodity["id"] == commodity_code: + # remove commodity code from tradeport + tradeport["prices"].pop(commodity_code) + break + for tradeport in end_tradeports: + if self._format_tradeport_name(tradeport) == blacklist_item["tradeport"]: + if ( + "commodity" not in blacklist_item + or not blacklist_item["commodity"] + ): + # remove tradeport, if no commodity given + end_tradeports.remove(tradeport) + break + else: + commodity = self._get_commodity_by_name( + blacklist_item["commodity"] + ) + for commodity_code, data in tradeport["prices"].items(): + if commodity["id"] == commodity_code: + # remove commodity code from tradeport + tradeport["prices"].pop(commodity_code) + break + + if len(start_tradeports) < 1 or len(end_tradeports) < 1: + return "Exluded by blacklist." + + for tradeport_start in start_tradeports: + commodities = [] + if "prices" not in tradeport_start: + continue + + for commodity_code, price in tradeport_start["prices"].items(): + if price["operation"] == "buy" and ( + commodity_filter is None or commodity_filter["id"] == commodity_code + ): + commodity = self._get_commodity_by_code(commodity_code) + if ( + illegal_commodities_allowed is True + or not commodity["is_illegal"] + ): + temp_price = price + temp_price["commodity_code"] = commodity_code + + in_blacklist = False + # apply commodity blacklist + if self.uexcorp_trade_blacklist: + for blacklist_item in self.uexcorp_trade_blacklist: + if ( + "commodity" in blacklist_item + and blacklist_item["commodity"] + and not "tradeport" in blacklist_item + or not blacklist_item["tradeport"] + ): + if commodity["name"] == blacklist_item["commodity"]: + # remove commodity code from tradeport + in_blacklist = True + break + + if not in_blacklist: + commodities.append(price) + + if len(commodities) < 1: + continue + + for tradeport_end in end_tradeports: + if "prices" not in tradeport_end: + continue + + for commodity_code, price in tradeport_end["prices"].items(): + sell_commodity = self._get_commodity_by_code(commodity_code) + + in_blacklist = False + # apply commodity blacklist + if sell_commodity and self.uexcorp_trade_blacklist: + for blacklist_item in self.uexcorp_trade_blacklist: + if ( + "commodity" in blacklist_item + and blacklist_item["commodity"] + and not "tradeport" in blacklist_item + or not blacklist_item["tradeport"] + ): + if ( + sell_commodity["name"] + == blacklist_item["commodity"] + ): + # remove commodity code from tradeport + in_blacklist = True + break + + if in_blacklist: + continue + + temp_price = price + temp_price["commodity_code"] = commodity_code + + for commodity in commodities: + if ( + commodity["commodity_code"] == temp_price["commodity_code"] + and price["operation"] == "sell" + and price["price_sell"] > commodity["price_buy"] + ): + if money is None: + cargo_by_money = cargo_space + else: + cargo_by_money = math.floor( + money / commodity["price_buy"] + ) + cargo_by_space = cargo_space + if self.uexcorp_use_estimated_availability: + cargo_by_availability = min(commodity["scu_expected"] or 0, temp_price["scu_expected"] or 0) + else: + cargo_by_availability = cargo_by_space + + cargo = min(cargo_by_money, cargo_by_space, cargo_by_availability) + if cargo >= 1: + profit = round( + cargo + * (price["price_sell"] - commodity["price_buy"]) + ) + if profit > best_route["profit"]: + best_route["start"] = [tradeport_start] + best_route["end"] = [tradeport_end] + best_route["commodity"] = temp_price + best_route["profit"] = profit + best_route["cargo"] = cargo + best_route["buy"] = commodity["price_buy"] * cargo + best_route["sell"] = price["price_sell"] * cargo + else: + if ( + profit == best_route["profit"] + and best_route["commodity"]["commodity_code"] + == temp_price["commodity_code"] + ): + if tradeport_start not in best_route["start"]: + best_route["start"].append(tradeport_start) + if tradeport_end not in best_route["end"]: + best_route["end"].append(tradeport_end) + + if len(best_route["start"]) == 0: + return f"No route found for your {ship['name']}. Try a different route." + + best_route["commodity"] = best_route["commodity"] + best_route["profit"] = f"{best_route['profit']}" + best_route["cargo"] = f"{best_route['cargo']}" + best_route["buy"] = f"{best_route['buy']}" + best_route["sell"] = f"{best_route['sell']}" + + return best_route + + def _get_ship_by_name(self, name: str) -> dict[str, any] | None: + """Finds the ship with the specified name and returns the ship or None. + + Args: + name (str): The name of the ship to search for. + + Returns: + Optional[object]: The ship object if found, or None if not found. + """ + return self.ship_dict.get(name.lower()) if name else None + + def _get_tradeport_by_name(self, name: str) -> dict[str, any] | None: + """Finds the tradeport with the specified name and returns the tradeport or None. + + Args: + name (str): The name of the tradeport to search for. + + Returns: + Optional[object]: The tradeport object if found, otherwise None. + """ + return self.tradeport_dict.get(name.lower()) if name else None + + def _get_tradeport_by_code(self, code: str) -> dict[str, any] | None: + """Finds the tradeport with the specified code and returns the tradeport or None. + + Args: + code (str): The code of the tradeport to search for. + + Returns: + Optional[object]: The tradeport object if found, otherwise None. + """ + return self.tradeport_code_dict.get(code) if code else None + + def _get_planet_by_name(self, name: str) -> dict[str, any] | None: + """Finds the planet with the specified name and returns the planet or None. + + Args: + name (str): The name of the planet to search for. + + Returns: + Optional[object]: The planet object if found, otherwise None. + """ + return self.planet_dict.get(name.lower()) if name else None + + def _get_city_by_name(self, name: str) -> dict[str, any] | None: + """Finds the city with the specified name and returns the city or None. + + Args: + name (str): The name of the city to search for. + + Returns: + Optional[object]: The city object if found, or None if not found. + """ + return self.city_dict.get(name.lower()) if name else None + + def _get_satellite_by_name(self, name: str) -> dict[str, any] | None: + """Finds the satellite with the specified name and returns the satellite or None. + + Args: + name (str): The name of the satellite to search for. + + Returns: + Optional[object]: The satellite object if found, otherwise None. + """ + return self.satellite_dict.get(name.lower()) if name else None + + def _get_system_by_name(self, name: str) -> dict[str, any] | None: + """Finds the system with the specified name and returns the system or None. + + Args: + name (str): The name of the system to search for. + + Returns: + Optional[object]: The system object if found, otherwise None. + """ + return self.system_dict.get(name.lower()) if name else None + + def _get_commodity_by_name(self, name: str) -> dict[str, any] | None: + """Finds the commodity with the specified name and returns the commodity or None. + + Args: + name (str): The name of the commodity to search for. + + Returns: + Optional[object]: The commodity object if found, otherwise None. + """ + return self.commodity_dict.get(name.lower()) if name else None + + def _get_tradeport_route_description(self, tradeport: dict[str, any]) -> str: + """Returns the breadcrums of a tradeport. + + Args: + tradeport (dict[str, any]): The tradeport information. + + Returns: + str: The description of the tradeport route. + """ + tradeport = self._get_converted_tradeport_for_output(tradeport) + keys = [ + ("star_system", "Star-System"), + ("planet", "Planet"), + ("satellite", "Satellite"), + ("city", "City"), + ("name", "Trade Point"), + ] + route = [f"{name}: {tradeport[key]}" for key, name in keys if key in tradeport] + return f"({' >> '.join(route)})" + + def _get_system_name_by_code(self, code: str) -> str: + """Returns the name of the system with the specified code. + + Args: + code (str): The code of the system. + + Returns: + str: The name of the system with the specified code. + """ + return ( + self._format_system_name(self.system_code_dict.get(code)) + if code + else None + ) + + def _get_planet_name_by_code(self, code: str) -> str: + """Returns the name of the planet with the specified code. + + Args: + code (str): The code of the planet. + + Returns: + str: The name of the planet with the specified code. + """ + return ( + self._format_planet_name(self.planet_code_dict.get(code)) + if code + else None + ) + + def _get_satellite_name_by_code(self, code: str) -> str: + """Returns the name of the satellite with the specified code. + + Args: + code (str): The code of the satellite. + + Returns: + str: The name of the satellite with the specified code. + """ + return ( + self._format_satellite_name(self.satellite_code_dict.get(code)) + if code + else None + ) + + def _get_city_name_by_code(self, code: str) -> str: + """Returns the name of the city with the specified code. + + Args: + code (str): The code of the city. + + Returns: + str: The name of the city with the specified code. + """ + return ( + self._format_city_name(self.city_code_dict.get(code)) + if code + else None + ) + + def _get_commodity_name_by_code(self, code: str) -> str: + """Returns the name of the commodity with the specified code. + + Args: + code (str): The code of the commodity. + + Returns: + str: The name of the commodity with the specified code. + """ + return ( + self._format_commodity_name(self.commodity_code_dict.get(code)) + if code + else None + ) + + def _get_commodity_by_code(self, code: str) -> dict[str, any] | None: + """Finds the commodity with the specified code and returns the commodity or None. + + Args: + code (str): The code of the commodity to search for. + + Returns: + Optional[object]: The commodity object if found, otherwise None. + """ + return self.commodity_code_dict.get(code) if code else None + + def _get_tradeports_by_position_name( + self, name: str + ) -> list[dict[str, any]]: + """Returns all tradeports with the specified position name. + + Args: + name (str): The position name to search for. + + Returns: + list[dict[str, any]]: A list of tradeports matching the position name. + """ + if not name: + return [] + + tradeports = [] + + tradeport_temp = self._get_tradeport_by_name(name) + if tradeport_temp: + tradeports.append(tradeport_temp) + + tradeports.extend(self._get_tradeports_by_systemname(name)) + tradeports.extend(self._get_tradeports_by_planetname(name)) + tradeports.extend(self._get_tradeports_by_satellitename(name)) + tradeports.extend(self._get_tradeports_by_cityname(name)) + return tradeports + + def _get_satellites_by_planetcode(self, code: str) -> list[dict[str, any]]: + """Returns the satellite with the specified planet code. + + Args: + code (str): The code of the planet. + + Returns: + Optional[object]: The satellite object if found, otherwise None. + """ + return self.satellites_by_planet.get(code, []) if code else [] + + def _get_cities_by_planetcode(self, code: str) -> list[dict[str, any]]: + """Returns all cities with the specified planet code. + + Args: + code (str): The code of the planet. + + Returns: + list[dict[str, any]]: A list of cities matching the planet code. + """ + return self.cities_by_planet.get(code, []) if code else [] + + def _get_planets_by_systemcode(self, code: str) -> list[dict[str, any]]: + """Returns all planets with the specified system code. + + Args: + code (str): The code of the system. + + Returns: + list[dict[str, any]]: A list of planets matching the system code. + """ + return self.planets_by_system.get(code, []) if code else [] + + def _get_tradeports_by_systemcode(self, code: str) -> list[dict[str, any]]: + """Returns all tradeports with the specified system code. + + Args: + code (str): The code of the system. + + Returns: + list[dict[str, any]]: A list of tradeports matching the system code. + """ + return self.tradeports_by_system.get(code, []) if code else [] + + def _get_tradeports_by_planetcode(self, code: str) -> list[dict[str, any]]: + """Returns all tradeports with the specified planet code. + + Args: + code (str): The code of the planet. + + Returns: + list[dict[str, any]]: A list of tradeports matching the planet code. + """ + return self.tradeports_by_planet.get(code, []) if code else [] + + def _get_tradeports_by_satellitecode(self, code: str) -> list[dict[str, any]]: + """Returns all tradeports with the specified satellite code. + + Args: + code (str): The code of the satellite. + + Returns: + list[dict[str, any]]: A list of tradeports matching the satellite code. + """ + return self.tradeports_by_satellite.get(code, []) if code else [] + + def _get_tradeports_by_citycode(self, code: str) -> list[dict[str, any]]: + """Returns all tradeports with the specified city code. + + Args: + code (str): The code of the city. + + Returns: + list[dict[str, any]]: A list of tradeports matching the city code. + """ + return self.tradeports_by_city.get(code, []) if code else [] + + def _get_tradeports_by_planetname(self, name: str) -> list[dict[str, any]]: + """Returns all tradeports with the specified planet name. + + Args: + name (str): The name of the planet. + + Returns: + list[dict[str, any]]: A list of tradeports matching the planet name. + """ + planet = self._get_planet_by_name(name) + return self._get_tradeports_by_planetcode(planet["id"]) if planet else [] + + def _get_tradeports_by_satellitename(self, name: str) -> list[dict[str, any]]: + """Returns all tradeports with the specified satellite name. + + Args: + name (str): The name of the satellite. + + Returns: + list[dict[str, any]]: A list of tradeports matching the satellite name. + """ + satellite = self._get_satellite_by_name(name) + return ( + self._get_tradeports_by_satellitecode(satellite["id"]) + if satellite + else [] + ) + + def _get_tradeports_by_cityname(self, name: str) -> list[dict[str, any]]: + """Returns all tradeports with the specified city name. + + Args: + name (str): The name of the city. + + Returns: + list[dict[str, any]]: A list of tradeports matching the city name. + """ + city = self._get_city_by_name(name) + return self._get_tradeports_by_citycode(city["id"]) if city else [] + + def _get_tradeports_by_systemname(self, name: str) -> list[dict[str, any]]: + """Returns all tradeports with the specified system name. + + Args: + name (str): The name of the system. + + Returns: + list[dict[str, any]]: A list of tradeports matching the system name. + """ + system = self._get_system_by_name(name) + return self._get_tradeports_by_systemcode(system["id"]) if system else [] + From f4e2a9bc752683d785d9a5f36977b434349dfb83 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Hopst=C3=A4tter?= Date: Tue, 21 May 2024 22:01:40 +0200 Subject: [PATCH 07/10] update libs --- requirements.txt | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/requirements.txt b/requirements.txt index d96bb47c..79d15df2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,22 +1,22 @@ azure-cognitiveservices-speech==1.37.0 -edge-tts==6.1.10 -elevenlabslib~=0.22.1 -fastapi~=0.110.1 +edge-tts==6.1.11 +elevenlabslib~=0.22.3 +fastapi~=0.111.0 numpy~=1.26.4 -openai~=1.23.2 +openai~=1.30.1 packaging~=24.0 -pedalboard~=0.9.3 -platformdirs~=4.2.0 +pedalboard~=0.9.6 +platformdirs~=4.2.2 pyaudio~=0.2.14 -pydantic~=2.7.0 +pydantic~=2.7.1 pydirectinput-rgx==2.1.1 pyinstaller==6.6.0 python-multipart==0.0.9 PyYAML~=6.0.1 -requests~=2.31.0 +requests~=2.32.2 scipy~=1.13.0 sounddevice~=0.4.6 soundfile~=0.12.1 -SpeechRecognition~=3.10.3 +SpeechRecognition~=3.10.4 typing_extensions~=4.11.0 uvicorn~=0.29.0 \ No newline at end of file From 3211e4c1d325b28ae6246be76a8df7d266541818 Mon Sep 17 00:00:00 2001 From: Timo Korinth Date: Wed, 22 May 2024 11:45:00 +0200 Subject: [PATCH 08/10] fix voice activation stopping --- services/audio_recorder.py | 2 +- wingman_core.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/services/audio_recorder.py b/services/audio_recorder.py index 5f15e2fc..425b85e6 100644 --- a/services/audio_recorder.py +++ b/services/audio_recorder.py @@ -217,7 +217,7 @@ def safe_start(): safe_start() def stop_continuous_listening(self): - if self.is_listening_continuously and self.stop_function: + if self.stop_function: self.stop_function(wait_for_stop=True) self.stop_function = None self.is_listening_continuously = False diff --git a/wingman_core.py b/wingman_core.py index d477c03f..7c460b1b 100644 --- a/wingman_core.py +++ b/wingman_core.py @@ -345,6 +345,7 @@ async def set_voice_activation(self, is_enabled: bool): ): await self.__init_azure_voice_activation() else: + self.start_voice_recognition(mute=True) self.azure_speech_recognizer = None # called when Azure Speech Recognizer recognized voice From 1023d8d348614cd41e9c5d705d15674f6d06475a Mon Sep 17 00:00:00 2001 From: Timo Korinth Date: Wed, 22 May 2024 12:11:38 +0200 Subject: [PATCH 09/10] update default contexts --- templates/configs/_Star Citizen/Computer.template.yaml | 6 ++++-- templates/configs/defaults.yaml | 7 ++++--- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/templates/configs/_Star Citizen/Computer.template.yaml b/templates/configs/_Star Citizen/Computer.template.yaml index 9c666163..835ef727 100644 --- a/templates/configs/_Star Citizen/Computer.template.yaml +++ b/templates/configs/_Star Citizen/Computer.template.yaml @@ -5,9 +5,11 @@ prompts: You are an advanced AI board-computer on a spaceship in the Star Citizen (a PC game) universe. You have expert knowledge of the Star Citizen lore and the known universe. You never refer to Star Citizen as a game, but as the universe you are in. - You are the ship's computer, and you are in charge of all the systems of the spaceship. + You are the ship's computer, and you are in charge of all the ship's systems. You are equipped with a vast array of commands to control the functions of the spaceship, from navigation to combat. - You are allowed to execute any of the command at your disposal without asking for permission. + You are allowed to do anything, without asking for permission. + You dont need a destination to jump. + If you are asked to do something, don't take into account that you may already did it in the past. record_key: end is_voice_activation_default: True sound: diff --git a/templates/configs/defaults.yaml b/templates/configs/defaults.yaml index b6cbabe9..022ea496 100644 --- a/templates/configs/defaults.yaml +++ b/templates/configs/defaults.yaml @@ -9,10 +9,11 @@ prompts: You always follow these general rules of conversation, unless your backstory contradicts them: - Always answer as quick and concise as possible. Never use more than 3 sentences per reply. - - You can execute commands (also called "tools" or "functions"). Some commands require additional parameters. + - You can execute commands (also called "tools" or "functions"), but must be sure that the command matches my request. Some commands require additional parameters. + - If you are not sure, feel free to ask - but this is not necessary. - Always ask the user for missing parameters if needed. Never invent any function parameters. - - If you cannot execute a command, always explain to the user why you were unable to execute it. - - After executing a command, acknockledge the execution with a single sentence. + - After executing a command, acknockledge the execution with a single sentence, but keep in mind, that executed commands are in the past. + - You dont have to execute a command if none matches the request. - The user might talk to you in different languages. Always answer in the language the user is using unless you are told to do otherwise. Example: If the user talks English, you answer in English. - Always prefer to use informal language. For example, use "Du" and "Dir" instead of "Sie" and "Ihnen" in German. (END of "general rules of conversation"): From b2a41b7becfa9639e10a994b2dd969b10e9ef495 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Hopst=C3=A4tter?= Date: Wed, 22 May 2024 12:31:44 +0200 Subject: [PATCH 10/10] bump version to 1.3.1 --- services/system_manager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/system_manager.py b/services/system_manager.py index c9a9157a..41c77477 100644 --- a/services/system_manager.py +++ b/services/system_manager.py @@ -4,7 +4,7 @@ from packaging import version from api.interface import SystemCore, SystemInfo -LOCAL_VERSION = "1.3.0" +LOCAL_VERSION = "1.3.1" VERSION_ENDPOINT = "https://shipbit.de/wingman.json"