Skip to content

Commit 18518fe

Browse files
authoredJan 29, 2025
update StarHead skill (#278)
* add shop info * update prompt * try another approach * update function naming * fix naming * first try to integrate vision skill in starhead * update prompts and image generation skill * refactor after rebase * remove vision functions for now * update template
1 parent fe496f7 commit 18518fe

File tree

6 files changed

+335
-95
lines changed

6 files changed

+335
-95
lines changed
 

‎skills/image_generation/main.py

+33-29
Original file line numberDiff line numberDiff line change
@@ -39,36 +39,40 @@ async def execute_tool(
3939

4040
if tool_name == "generate_image":
4141
prompt = parameters["prompt"]
42-
if self.settings.debug_mode:
43-
await self.printr.print_async(f"Generate image with prompt: {prompt}.", color=LogType.INFO)
44-
image = await self.wingman.generate_image(prompt)
45-
await self.printr.print_async(
46-
"",
47-
color=LogType.INFO,
48-
source=LogSource.WINGMAN,
49-
source_name=self.wingman.name,
50-
skill_name=self.name,
51-
additional_data={"image_url": image},
52-
)
53-
if image:
54-
function_response = "Here is an image based on your prompt."
55-
56-
if self.save_images:
57-
image_path = path.join(
58-
self.image_path,
59-
f"{datetime.datetime.now().strftime('%Y-%m-%d_%H-%M-%S')}_{prompt[:40]}.png"
60-
)
61-
image_response = requests.get(image)
62-
63-
if image_response.status_code == 200:
64-
with open(image_path, 'wb') as file:
65-
file.write(image_response.content)
66-
67-
function_response += f" The image has also been stored to {image_path}."
68-
if self.settings.debug_mode:
69-
await self.printr.print_async(f"Image displayed and saved at {image_path}.", color=LogType.INFO)
70-
42+
await self.generate_image(prompt)
43+
function_response = "Here is an image based on your prompt."
7144
return function_response, instant_response
45+
46+
async def generate_image(self, prompt: str) -> str:
47+
if self.settings.debug_mode:
48+
await self.printr.print_async(f"Generate image with prompt: {prompt}.", color=LogType.INFO)
49+
50+
image = await self.wingman.generate_image(prompt)
51+
await self.printr.print_async(
52+
"",
53+
color=LogType.INFO,
54+
source=LogSource.WINGMAN,
55+
source_name=self.wingman.name,
56+
skill_name=self.name,
57+
additional_data={"image_url": image},
58+
)
59+
if image:
60+
function_response = "Here is an image based on your prompt."
61+
62+
if self.save_images:
63+
image_path = path.join(
64+
self.image_path,
65+
f"{datetime.datetime.now().strftime('%Y-%m-%d_%H-%M-%S')}_{prompt[:40]}.png"
66+
)
67+
image_response = requests.get(image)
68+
69+
if image_response.status_code == 200:
70+
with open(image_path, 'wb') as file:
71+
file.write(image_response.content)
72+
73+
function_response += f" The image has also been stored to {image_path}."
74+
if self.settings.debug_mode:
75+
await self.printr.print_async(f"Image displayed and saved at {image_path}.", color=LogType.INFO)
7276

7377
async def is_waiting_response_needed(self, tool_name: str) -> bool:
7478
return True

‎skills/star_head/default_config.yaml

+3
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,9 @@ prompt: |
3131
Your job is to find good trading routes for the user based on his/her ship, current location and available budget.
3232
The user can also ask you about details of specific ships, components, weapons, and more.
3333
You always use the tools available to you to retrieve the required information and to provide the user with the information.
34+
Do not try to determine a parent object by yourself, always ask the user about it.
35+
Don't provide possible values of a parameter, always ask the user about it.
36+
The currency is always 'aUEC' spelled 'Alpha UEC'.
3437
custom_properties:
3538
- hint: The URL of the StarHead API.
3639
id: starhead_api_url

‎skills/star_head/main.py

+116-5
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,9 @@ def __init__(
3737
self.celestial_objects = []
3838
self.celestial_object_names = []
3939
self.quantum_drives = []
40+
self.shops = []
41+
self.shop_names = []
42+
self.shop_parent_names = []
4043

4144
async def validate(self) -> list[WingmanInitializationError]:
4245
errors = await super().validate()
@@ -78,6 +81,19 @@ async def _prepare_data(self):
7881
"vehiclecomponent", {"typeFilter": 8}
7982
)
8083

84+
self.shops = await self._fetch_data("shop")
85+
self.shop_names = [shop["name"] for shop in self.shops]
86+
87+
# Remove duplicate shop names
88+
self.shop_names = list(dict.fromkeys(self.shop_names))
89+
90+
self.shop_parent_names = [
91+
shop["parent"]["name"] for shop in self.shops if shop["parent"]
92+
]
93+
94+
# Remove duplicate parent names
95+
self.shop_parent_names = list(dict.fromkeys(self.shop_parent_names))
96+
8197
async def _fetch_data(
8298
self, endpoint: str, params: Optional[dict[str, any]] = None
8399
) -> list[dict[str, any]]:
@@ -113,6 +129,17 @@ async def execute_tool(
113129
function_response = await self._get_best_trading_route(**parameters)
114130
if tool_name == "get_ship_information":
115131
function_response = await self._get_ship_information(**parameters)
132+
if tool_name == "get_trading_information_of_specific_shop":
133+
function_response = await self._get_trading_information_of_specific_shop(
134+
**parameters
135+
)
136+
if tool_name == "get_trading_shop_information_for_celestial_objects":
137+
function_response = (
138+
await self._get_trading_shop_information_for_celestial_objects(
139+
**parameters
140+
)
141+
)
142+
116143
return function_response, instant_response
117144

118145
async def is_waiting_response_needed(self, tool_name: str) -> bool:
@@ -135,9 +162,9 @@ def get_tools(self) -> list[tuple[str, dict]]:
135162
"type": "string",
136163
"enum": self.celestial_object_names,
137164
},
138-
"moneyToSpend": {"type": "number"},
165+
"money_to_spend": {"type": "number"},
139166
},
140-
"required": ["ship", "position", "moneyToSpend"],
167+
"required": ["ship", "position", "money_to_spend"],
141168
},
142169
},
143170
},
@@ -159,10 +186,94 @@ def get_tools(self) -> list[tuple[str, dict]]:
159186
},
160187
},
161188
),
189+
(
190+
"get_trading_information_of_specific_shop",
191+
{
192+
"type": "function",
193+
"function": {
194+
"name": "get_trading_information_of_specific_shop",
195+
"description": "Gives trading information about the given shop, like which commodities you can sell or buy and for which price. If the name of the shop is not unique, you have to specify the celestial object.",
196+
"parameters": {
197+
"type": "object",
198+
"properties": {
199+
"shop": {"type": "string", "enum": self.shop_names},
200+
},
201+
"required": ["shop"],
202+
},
203+
},
204+
},
205+
),
206+
(
207+
"get_trading_shop_information_for_celestial_objects",
208+
{
209+
"type": "function",
210+
"function": {
211+
"name": "get_trading_shop_information_for_celestial_objects",
212+
"description": "Gives trading information about the given celestial object, like which commodities you can sell or buy at which shop and for which price. All shops with shop items on that celestial object will be returned.",
213+
"parameters": {
214+
"type": "object",
215+
"properties": {
216+
"celestial_object": {
217+
"type": "string",
218+
"enum": self.celestial_object_names,
219+
},
220+
},
221+
"required": ["celestial_object"],
222+
},
223+
},
224+
},
225+
),
162226
]
163227

164228
return tools
165229

230+
async def _get_trading_shop_information_for_celestial_objects(
231+
self, celestial_object: str
232+
) -> str:
233+
object_id = self._get_celestial_object_id(celestial_object)
234+
235+
if not object_id:
236+
return f"Could not find celestial object '{celestial_object}' in the StarHead database."
237+
238+
shops = await self._fetch_data(f"shop?celestialObjectFilter={object_id}")
239+
240+
shop_items = {}
241+
for shop in shops:
242+
items = await self._fetch_data(f"shop/{shop['id']}/items")
243+
for item in items:
244+
item["pricePerItem"] = item["pricePerItem"] * 100
245+
item["tradeType"] = (
246+
"Sold by store" if item["tradeType"] == "buy" else "The shop buys"
247+
)
248+
shop_items[f"{shop['parent']['name']} - {shop['name']}"] = items
249+
250+
shop_details = json.dumps(shop_items)
251+
return shop_details
252+
253+
async def _get_trading_information_of_specific_shop(self, shop: str = None) -> str:
254+
# Get all shops with the given name
255+
shops = [
256+
shop for shop in self.shops if shop["name"].lower() == shop["name"].lower()
257+
]
258+
259+
# Check if there are multiple shops with the same name
260+
if len(shops) > 1:
261+
return f"Multiple shops with the name '{shop}' found. Please specify the celestial object."
262+
263+
if not shops:
264+
return f"Could not find shop '{shop}' in the StarHead database."
265+
266+
shop_items = {}
267+
268+
for shop in shops:
269+
items = await self._fetch_data(f"shop/{shop['id']}/items")
270+
for item in items:
271+
item["pricePerItem"] = item["pricePerItem"] * 100
272+
shop_items[shop["name"]] = items
273+
274+
shop_details = json.dumps(items)
275+
return shop_details
276+
166277
async def _get_ship_information(self, ship: str) -> str:
167278
try:
168279
response = requests.get(
@@ -177,7 +288,7 @@ async def _get_ship_information(self, ship: str) -> str:
177288
return ship_details
178289

179290
async def _get_best_trading_route(
180-
self, ship: str, position: str, moneyToSpend: float
291+
self, ship: str, position: str, money_to_spend: float
181292
) -> str:
182293
"""Calculates the best trading route for the specified ship and position.
183294
Note that the function arguments have to match the funtion_args from OpenAI, hence the camelCase!
@@ -195,7 +306,7 @@ async def _get_best_trading_route(
195306
"startCelestialObjectId": celestial_object_id,
196307
"quantumDriveId": qd["id"] if qd else None,
197308
"maxAvailablScu": cargo,
198-
"maxAvailableMoney": moneyToSpend,
309+
"maxAvailableMoney": money_to_spend,
199310
"useOnlyWeaponFreeZones": False,
200311
"onlySingleSections": True,
201312
}
@@ -215,7 +326,7 @@ async def _get_best_trading_route(
215326
if parsed_response:
216327
section = parsed_response[0]
217328
return json.dumps(section)
218-
return f"No route found for ship '{ship}' at '{position}' with '{moneyToSpend}' aUEC."
329+
return f"No route found for ship '{ship}' at '{position}' with '{money_to_spend}' aUEC."
219330

220331
def _get_celestial_object_id(self, name: str) -> Optional[int]:
221332
"""Finds the ID of the celestial object with the specified name."""

‎skills/vision_ai/main.py

+64-56
Original file line numberDiff line numberDiff line change
@@ -66,70 +66,78 @@ async def execute_tool(
6666
instant_response = ""
6767

6868
if tool_name == "analyse_what_you_or_user_sees":
69-
# Take a screenshot
70-
with mss() as sct:
71-
main_display = sct.monitors[self.display]
72-
screenshot = sct.grab(main_display)
73-
74-
# Create a PIL image from array
75-
image = Image.frombytes(
76-
"RGB", screenshot.size, screenshot.bgra, "raw", "BGRX"
77-
)
78-
79-
desired_width = 1000
80-
aspect_ratio = image.height / image.width
81-
new_height = int(desired_width * aspect_ratio)
82-
83-
resized_image = image.resize((desired_width, new_height))
8469

85-
png_base64 = self.pil_image_to_base64(resized_image)
70+
question = parameters.get("question", "What's in this image?")
71+
answer = await self.analyse_screen(question)
8672

87-
if self.show_screenshots:
73+
if answer:
74+
if self.settings.debug_mode:
8875
await self.printr.print_async(
89-
"Analyzing this image",
90-
color=LogType.INFO,
91-
source=LogSource.WINGMAN,
92-
source_name=self.wingman.name,
93-
skill_name=self.name,
94-
additional_data={"image_base64": png_base64},
76+
f"Vision analysis: {answer}.", color=LogType.INFO
9577
)
78+
function_response = answer
9679

97-
question = parameters.get("question", "What's in this image?")
80+
return function_response, instant_response
9881

99-
messages = [
100-
{
101-
"role": "system",
102-
"content": """
103-
You are a helpful ai assistant.
104-
""",
105-
},
106-
{
107-
"role": "user",
108-
"content": [
109-
{"type": "text", "text": question},
110-
{
111-
"type": "image_url",
112-
"image_url": {
113-
"url": f"data:image/jpeg;base64,{png_base64}",
114-
"detail": "high",
115-
},
116-
},
117-
],
118-
},
119-
]
120-
completion = await self.llm_call(messages)
121-
answer = (
122-
completion.choices[0].message.content
123-
if completion and completion.choices
124-
else ""
125-
)
82+
async def analyse_screen(self, prompt: str, desired_image_width: int = 1000):
83+
function_response = ""
12684

127-
if answer:
128-
if self.settings.debug_mode:
129-
await self.printr.print_async(f"Vision analysis: {answer}.", color=LogType.INFO)
130-
function_response = answer
85+
# Take a screenshot
86+
with mss() as sct:
87+
main_display = sct.monitors[self.display]
88+
screenshot = sct.grab(main_display)
13189

132-
return function_response, instant_response
90+
# Create a PIL image from array
91+
image = Image.frombytes(
92+
"RGB", screenshot.size, screenshot.bgra, "raw", "BGRX"
93+
)
94+
95+
aspect_ratio = image.height / image.width
96+
new_height = int(desired_image_width * aspect_ratio)
97+
98+
resized_image = image.resize((desired_image_width, new_height))
99+
100+
png_base64 = self.pil_image_to_base64(resized_image)
101+
102+
if self.show_screenshots:
103+
await self.printr.print_async(
104+
"Analyzing this image",
105+
color=LogType.INFO,
106+
source=LogSource.WINGMAN,
107+
source_name=self.wingman.name,
108+
skill_name=self.name,
109+
additional_data={"image_base64": png_base64},
110+
)
111+
112+
messages = [
113+
{
114+
"role": "system",
115+
"content": """
116+
You are a helpful ai assistant.
117+
""",
118+
},
119+
{
120+
"role": "user",
121+
"content": [
122+
{"type": "text", "text": prompt},
123+
{
124+
"type": "image_url",
125+
"image_url": {
126+
"url": f"data:image/jpeg;base64,{png_base64}",
127+
"detail": "high",
128+
},
129+
},
130+
],
131+
},
132+
]
133+
completion = await self.llm_call(messages)
134+
function_response = (
135+
completion.choices[0].message.content
136+
if completion and completion.choices
137+
else ""
138+
)
139+
140+
return function_response
133141

134142
async def is_summarize_needed(self, tool_name: str) -> bool:
135143
"""Returns whether a tool needs to be summarized."""

‎templates/skills/star_head/default_config.yaml

+3
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,9 @@ prompt: |
3131
Your job is to find good trading routes for the user based on his/her ship, current location and available budget.
3232
The user can also ask you about details of specific ships, components, weapons, and more.
3333
You always use the tools available to you to retrieve the required information and to provide the user with the information.
34+
Do not try to determine a parent object by yourself, always ask the user about it.
35+
Don't provide possible values of a parameter, always ask the user about it.
36+
The currency is always 'aUEC' spelled 'Alpha UEC'.
3437
custom_properties:
3538
- hint: The URL of the StarHead API.
3639
id: starhead_api_url

‎templates/skills/star_head/main.py

+116-5
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,9 @@ def __init__(
3737
self.celestial_objects = []
3838
self.celestial_object_names = []
3939
self.quantum_drives = []
40+
self.shops = []
41+
self.shop_names = []
42+
self.shop_parent_names = []
4043

4144
async def validate(self) -> list[WingmanInitializationError]:
4245
errors = await super().validate()
@@ -78,6 +81,19 @@ async def _prepare_data(self):
7881
"vehiclecomponent", {"typeFilter": 8}
7982
)
8083

84+
self.shops = await self._fetch_data("shop")
85+
self.shop_names = [shop["name"] for shop in self.shops]
86+
87+
# Remove duplicate shop names
88+
self.shop_names = list(dict.fromkeys(self.shop_names))
89+
90+
self.shop_parent_names = [
91+
shop["parent"]["name"] for shop in self.shops if shop["parent"]
92+
]
93+
94+
# Remove duplicate parent names
95+
self.shop_parent_names = list(dict.fromkeys(self.shop_parent_names))
96+
8197
async def _fetch_data(
8298
self, endpoint: str, params: Optional[dict[str, any]] = None
8399
) -> list[dict[str, any]]:
@@ -113,6 +129,17 @@ async def execute_tool(
113129
function_response = await self._get_best_trading_route(**parameters)
114130
if tool_name == "get_ship_information":
115131
function_response = await self._get_ship_information(**parameters)
132+
if tool_name == "get_trading_information_of_specific_shop":
133+
function_response = await self._get_trading_information_of_specific_shop(
134+
**parameters
135+
)
136+
if tool_name == "get_trading_shop_information_for_celestial_objects":
137+
function_response = (
138+
await self._get_trading_shop_information_for_celestial_objects(
139+
**parameters
140+
)
141+
)
142+
116143
return function_response, instant_response
117144

118145
async def is_waiting_response_needed(self, tool_name: str) -> bool:
@@ -135,9 +162,9 @@ def get_tools(self) -> list[tuple[str, dict]]:
135162
"type": "string",
136163
"enum": self.celestial_object_names,
137164
},
138-
"moneyToSpend": {"type": "number"},
165+
"money_to_spend": {"type": "number"},
139166
},
140-
"required": ["ship", "position", "moneyToSpend"],
167+
"required": ["ship", "position", "money_to_spend"],
141168
},
142169
},
143170
},
@@ -159,10 +186,94 @@ def get_tools(self) -> list[tuple[str, dict]]:
159186
},
160187
},
161188
),
189+
(
190+
"get_trading_information_of_specific_shop",
191+
{
192+
"type": "function",
193+
"function": {
194+
"name": "get_trading_information_of_specific_shop",
195+
"description": "Gives trading information about the given shop, like which commodities you can sell or buy and for which price. If the name of the shop is not unique, you have to specify the celestial object.",
196+
"parameters": {
197+
"type": "object",
198+
"properties": {
199+
"shop": {"type": "string", "enum": self.shop_names},
200+
},
201+
"required": ["shop"],
202+
},
203+
},
204+
},
205+
),
206+
(
207+
"get_trading_shop_information_for_celestial_objects",
208+
{
209+
"type": "function",
210+
"function": {
211+
"name": "get_trading_shop_information_for_celestial_objects",
212+
"description": "Gives trading information about the given celestial object, like which commodities you can sell or buy at which shop and for which price. All shops with shop items on that celestial object will be returned.",
213+
"parameters": {
214+
"type": "object",
215+
"properties": {
216+
"celestial_object": {
217+
"type": "string",
218+
"enum": self.celestial_object_names,
219+
},
220+
},
221+
"required": ["celestial_object"],
222+
},
223+
},
224+
},
225+
),
162226
]
163227

164228
return tools
165229

230+
async def _get_trading_shop_information_for_celestial_objects(
231+
self, celestial_object: str
232+
) -> str:
233+
object_id = self._get_celestial_object_id(celestial_object)
234+
235+
if not object_id:
236+
return f"Could not find celestial object '{celestial_object}' in the StarHead database."
237+
238+
shops = await self._fetch_data(f"shop?celestialObjectFilter={object_id}")
239+
240+
shop_items = {}
241+
for shop in shops:
242+
items = await self._fetch_data(f"shop/{shop['id']}/items")
243+
for item in items:
244+
item["pricePerItem"] = item["pricePerItem"] * 100
245+
item["tradeType"] = (
246+
"Sold by store" if item["tradeType"] == "buy" else "The shop buys"
247+
)
248+
shop_items[f"{shop['parent']['name']} - {shop['name']}"] = items
249+
250+
shop_details = json.dumps(shop_items)
251+
return shop_details
252+
253+
async def _get_trading_information_of_specific_shop(self, shop: str = None) -> str:
254+
# Get all shops with the given name
255+
shops = [
256+
shop for shop in self.shops if shop["name"].lower() == shop["name"].lower()
257+
]
258+
259+
# Check if there are multiple shops with the same name
260+
if len(shops) > 1:
261+
return f"Multiple shops with the name '{shop}' found. Please specify the celestial object."
262+
263+
if not shops:
264+
return f"Could not find shop '{shop}' in the StarHead database."
265+
266+
shop_items = {}
267+
268+
for shop in shops:
269+
items = await self._fetch_data(f"shop/{shop['id']}/items")
270+
for item in items:
271+
item["pricePerItem"] = item["pricePerItem"] * 100
272+
shop_items[shop["name"]] = items
273+
274+
shop_details = json.dumps(items)
275+
return shop_details
276+
166277
async def _get_ship_information(self, ship: str) -> str:
167278
try:
168279
response = requests.get(
@@ -177,7 +288,7 @@ async def _get_ship_information(self, ship: str) -> str:
177288
return ship_details
178289

179290
async def _get_best_trading_route(
180-
self, ship: str, position: str, moneyToSpend: float
291+
self, ship: str, position: str, money_to_spend: float
181292
) -> str:
182293
"""Calculates the best trading route for the specified ship and position.
183294
Note that the function arguments have to match the funtion_args from OpenAI, hence the camelCase!
@@ -195,7 +306,7 @@ async def _get_best_trading_route(
195306
"startCelestialObjectId": celestial_object_id,
196307
"quantumDriveId": qd["id"] if qd else None,
197308
"maxAvailablScu": cargo,
198-
"maxAvailableMoney": moneyToSpend,
309+
"maxAvailableMoney": money_to_spend,
199310
"useOnlyWeaponFreeZones": False,
200311
"onlySingleSections": True,
201312
}
@@ -215,7 +326,7 @@ async def _get_best_trading_route(
215326
if parsed_response:
216327
section = parsed_response[0]
217328
return json.dumps(section)
218-
return f"No route found for ship '{ship}' at '{position}' with '{moneyToSpend}' aUEC."
329+
return f"No route found for ship '{ship}' at '{position}' with '{money_to_spend}' aUEC."
219330

220331
def _get_celestial_object_id(self, name: str) -> Optional[int]:
221332
"""Finds the ID of the celestial object with the specified name."""

0 commit comments

Comments
 (0)
Please sign in to comment.