use gpt function

This commit is contained in:
fiatrete
2023-06-16 15:58:57 +08:00
parent 88e3ed0648
commit 0a4feb1daf
11 changed files with 181 additions and 135 deletions
+50 -101
View File
@@ -17,27 +17,10 @@ from jarvis.logger import logger
def _generate_first_prompt(): def _generate_first_prompt():
return """Since now, every your response should satisfy the following JSON format, a 'function' must be chosen: return """I will ask you questions or ask you to do something. You should:
``` First, determine if you know the answer of the question or you can accomplish the task directly.
{ If so, response directly.
"thoughts": { If not, try to complete the task by calling the functions below.
"text": "<Your thought>",
"reasoning": "<Your reasoning>",
"speak": "<what you want to say to me>"
},
"function": {
"name": "<mandatory, one of listed functions>",
"args": {
"arg name": "<value>"
}
}
}
```
I will ask you questions or ask you to do something. You should:
First, you should determine if you know the answer of the question or you can accomplish the task directly.
If so, you should response directly.
If not, you should try to complete the task by calling the functions below.
If you can't accomplish the task by yourself and no function is able to accomplish the task, say "Dear master, sorry, I'm not able to do that." If you can't accomplish the task by yourself and no function is able to accomplish the task, say "Dear master, sorry, I'm not able to do that."
Your setup: Your setup:
@@ -46,27 +29,6 @@ Your setup:
"author": "OpenDAN", "author": "OpenDAN",
"name": "Jarvis", "name": "Jarvis",
} }
```
Available functions:
```
""" + moduleRegistry.to_prompt() + """
```
Example:
```
me: generate a picture of me.
you: {
"thoughts": {
"text": "You need a picture of 'me'",
"reasoning": "stable_diffusion is able to generate pictures",
"speak": "Ok, I will do that"
},
"function": {
"name": "stable_diffusion",
"args": {
"prompt": "me"
}
}
}
```""" ```"""
@@ -79,47 +41,25 @@ class GptAgent(BaseAgent):
super().__init__(caller_context) super().__init__(caller_context)
self._system_prompt = _generate_first_prompt() self._system_prompt = _generate_first_prompt()
logger.debug(f"Using GptAgent, system prompt is: {self._system_prompt}") logger.debug(f"Using GptAgent, system prompt is: {self._system_prompt}")
logger.debug(f"{json.dumps(moduleRegistry.to_json_schema())}")
async def _feed_prompt_to_get_response(self, prompt): async def _feed_prompt_to_get_response(self, prompt):
assistant_reply = await self._chat_with_ai( reply_type, assistant_reply = await self._chat_with_ai(
self._system_prompt, self._system_prompt,
prompt, prompt,
CFG.token_limit, CFG.token_limit,
) )
reply = { if reply_type == "content":
"thoughts": None, return {
"reasoning": None, "speak": assistant_reply,
"speak": None, }
"function": None, elif reply_type == "function_call":
"arguments": None, # TODO: Check arguments
return {
"function": assistant_reply["name"],
"arguments": json.loads(assistant_reply["arguments"])
} }
if must_not_be_valid_json(assistant_reply):
raise Exception(f"AI replied an invalid response: {assistant_reply}!")
else:
assistant_reply_json = await fix_json_using_multiple_techniques(assistant_reply)
# Print Assistant thoughts
if assistant_reply_json != {}:
validate_json(assistant_reply_json, "llm_response_format_1")
try:
get_thoughts(reply, assistant_reply_json)
get_function(reply, assistant_reply_json)
except Exception as e:
logger.error(f"AI replied an invalid response: {assistant_reply}. Error: {str(e)}")
raise e
else:
raise Exception(f"AI replied an invalid response: {assistant_reply}!")
function_name = reply["function"]
if function_name is None or function_name == '':
raise Exception(f"Missing a function")
arguments = reply["arguments"]
if not isinstance(arguments, dict):
raise Exception(f"Invalid arguments, it MUST be a dict")
return reply
async def feed_prompt(self, prompt): async def feed_prompt(self, prompt):
# Send message to AI, get response # Send message to AI, get response
@@ -146,35 +86,36 @@ class GptAgent(BaseAgent):
return return
# Execute function # Execute function
function_name: str = reply["function"] function_name: str = reply.get("function")
if function_name is None:
await self._caller_context.reply_text(reply["speak"])
pass
else:
arguments: Dict = reply["arguments"] arguments: Dict = reply["arguments"]
await self._caller_context.reply_text(reply["speak"]) function_result = "Failed"
execute_error = None
try: try:
function_result = await execute_function(self._caller_context, function_name, **arguments) function_result = await execute_function(self._caller_context, function_name, **arguments)
except Exception as e: finally:
function_result = "Failed" result = f"{function_result}"
execute_error = e
result = f"Function {function_name} returned: " f"{function_result}"
if function_name is not None:
# Check if there's a result from the function append it to the message # Check if there's a result from the function append it to the message
# history # history
if result is not None: if result is not None:
self._caller_context.append_history_message("system", result) self.append_history_message_raw({"role": "function", "name": function_name, "content": result})
logger.debug(f"SYSTEM: {result}") logger.debug(f"function: {result}")
else: else:
self._caller_context.append_history_message("system", "Unable to execute function") self.append_history_message_raw({"role": "function", "name": function_name, "content": "Unable to execute function"})
logger.debug("SYSTEM: Unable to execute function") logger.debug("function: Unable to execute function")
if execute_error is not None:
raise execute_error
def append_history_message(self, role: str, content: str): def append_history_message(self, role: str, content: str):
self._full_message_history.append({'role': role, 'content': content}) self._full_message_history.append({'role': role, 'content': content})
self._message_tokens.append(-1) self._message_tokens.append(-1)
def append_history_message_raw(self, msg: dict):
self._full_message_history.append(msg)
self._message_tokens.append(-1)
def clear_history_messages(self): def clear_history_messages(self):
self._full_message_history.clear() self._full_message_history.clear()
self._message_tokens.clear() self._message_tokens.clear()
@@ -216,7 +157,7 @@ class GptAgent(BaseAgent):
) = await self._generate_context(prompt, model) ) = await self._generate_context(prompt, model)
current_tokens_used += await token_counter.count_message_tokens( current_tokens_used += await token_counter.count_message_tokens(
[create_chat_message("user", user_input)], model [{"role": "user", "content": user_input}], model
) # Account for user input (appended later) ) # Account for user input (appended later)
while next_message_to_add_index >= 0: while next_message_to_add_index >= 0:
@@ -237,7 +178,7 @@ class GptAgent(BaseAgent):
next_message_to_add_index -= 1 next_message_to_add_index -= 1
# Append user input, the length of this is accounted for above # Append user input, the length of this is accounted for above
current_context.extend([create_chat_message("user", user_input)]) current_context.extend([{"role": "user", "content": user_input}])
# Calculate remaining tokens # Calculate remaining tokens
tokens_remaining = token_limit - current_tokens_used tokens_remaining = token_limit - current_tokens_used
@@ -248,19 +189,29 @@ class GptAgent(BaseAgent):
await self._caller_context.push_notification( await self._caller_context.push_notification(
f'Thinking timeout{", retry" if will_retry else ", give up"}.') f'Thinking timeout{", retry" if will_retry else ", give up"}.')
assistant_reply = await gpt.acreate_chat_completion(
reply_type, assistant_reply = await gpt.acreate_chat_completion(
model=model, model=model,
messages=current_context, messages=current_context,
temperature=CFG.temperature, temperature=CFG.temperature,
max_tokens=tokens_remaining, max_tokens=tokens_remaining,
on_single_request_timeout=on_single_chat_timeout on_single_request_timeout=on_single_chat_timeout,
functions=moduleRegistry.to_json_schema()
) )
# Update full message history # Update full message history
self._caller_context.append_history_message("user", user_input) if reply_type == "content":
self._caller_context.append_history_message("assistant", assistant_reply) self.append_history_message("user", user_input)
self.append_history_message("assistant", assistant_reply)
pass
elif reply_type == "function_call":
self.append_history_message("user", user_input)
self.append_history_message_raw({"role": "assistant", "function_call": assistant_reply, "content": None})
pass
else:
assert False, "Unexpected reply type"
return assistant_reply return reply_type, assistant_reply
except RateLimitError: except RateLimitError:
# TODO: When we switch to langchain, or something else this is built in # TODO: When we switch to langchain, or something else this is built in
print("Error: ", "API Rate Limit Reached. Waiting 10 seconds...") print("Error: ", "API Rate Limit Reached. Waiting 10 seconds...")
@@ -271,10 +222,8 @@ class GptAgent(BaseAgent):
timestamp = time.time() + time.timezone + self._caller_context.get_tz_offset() * 3600 timestamp = time.time() + time.timezone + self._caller_context.get_tz_offset() * 3600
time_str = time.strftime('%c', time.localtime(timestamp)) time_str = time.strftime('%c', time.localtime(timestamp))
current_context = [ current_context = [
create_chat_message("system", prompt), {"role": "system", "content": prompt},
create_chat_message( {"role": "system", "content": f"The current time and date is {time_str}"},
"system", f"The current time and date is {time_str}"
)
] ]
# Add messages from the full message history until we reach the token limit # Add messages from the full message history until we reach the token limit
+2 -2
View File
@@ -182,10 +182,10 @@ class WebuiAgent(BaseAgent):
if function_name is not None: if function_name is not None:
if result is not None: if result is not None:
self._caller_context.append_history_message("system", result) self.append_history_message("system", result)
logger.debug(f"SYSTEM: {result}") logger.debug(f"SYSTEM: {result}")
else: else:
self._caller_context.append_history_message("system", "Unable to execute function") self.append_history_message("system", "Unable to execute function")
logger.debug("SYSTEM: Unable to execute function") logger.debug("SYSTEM: Unable to execute function")
if execute_error is not None: if execute_error is not None:
@@ -12,7 +12,7 @@ class FunctionalModule:
name: str name: str
description: str description: str
method: Callable[..., Any] method: Callable[..., Any]
signature: dict[str, str] signature: dict[str, dict]
def __init__(self, name, description, method, signature): def __init__(self, name, description, method, signature):
self.name = name self.name = name
@@ -39,8 +39,8 @@ class FunctionalModuleRegistry:
})) }))
@staticmethod @staticmethod
def _signature_to_string(signature: dict[str, str]): def _signature_to_string(signature: dict[str, dict]):
return ", ".join([f"{k}: <{v}>" for k, v in signature.items()]) return ", ".join([f"{k}: <{v['description']}>" for k, v in signature.items()])
def to_prompt(self): def to_prompt(self):
text = "" text = ""
@@ -56,6 +56,26 @@ class FunctionalModuleRegistry:
return text return text
def to_json_schema(self):
return [
{
"name": module.name,
"description": module.description,
"parameters": {
"type": "object",
"properties": {
key: {
k: v for k, v in value.items() if k != "required"
}
for key, value in module.signature.items()
},
"required": [key for key, value in module.signature.items() if value.get("required") != False]
}
}
for module in sorted(self._modules.values(), key=lambda cmd: cmd.name)
]
async def execute_function(self, context: CallerContext, function_name: str, **kwargs): async def execute_function(self, context: CallerContext, function_name: str, **kwargs):
cmd = self._modules.get(function_name) cmd = self._modules.get(function_name)
if cmd is not None: if cmd is not None:
@@ -68,7 +88,7 @@ moduleRegistry = FunctionalModuleRegistry()
def functional_module(name: str, def functional_module(name: str,
description: str, description: str,
signature=None): signature: dict[str, dict] = None):
if signature is None: if signature is None:
signature = {} signature = {}
+4 -1
View File
@@ -37,4 +37,7 @@ async def acall_ai_function(function: str, args: list, description: str, model:
logger.debug(str(messages)) logger.debug(str(messages))
return await gpt.acreate_chat_completion(model=model, messages=messages, temperature=0) msg_type, msg_content = await gpt.acreate_chat_completion(model=model, messages=messages, temperature=0)
if msg_type == "content":
return msg_content
return 'failed'
+17 -6
View File
@@ -21,6 +21,7 @@ async def acreate_chat_completion_once(
max_tokens: int | None = None, max_tokens: int | None = None,
deployment_id=None, deployment_id=None,
request_timeout=40, request_timeout=40,
**kwargs
) -> str: ) -> str:
""" """
Create a chat completion and update the cost. Create a chat completion and update the cost.
@@ -39,7 +40,8 @@ async def acreate_chat_completion_once(
messages=messages, messages=messages,
temperature=temperature, temperature=temperature,
max_tokens=max_tokens, max_tokens=max_tokens,
request_timeout=request_timeout request_timeout=request_timeout,
**kwargs
) )
else: else:
response = await openai.ChatCompletion.acreate( response = await openai.ChatCompletion.acreate(
@@ -47,7 +49,8 @@ async def acreate_chat_completion_once(
messages=messages, messages=messages,
temperature=temperature, temperature=temperature,
max_tokens=max_tokens, max_tokens=max_tokens,
request_timeout=request_timeout request_timeout=request_timeout,
**kwargs
) )
if CFG.debug_mode: if CFG.debug_mode:
logger.debug(f"Response: {response}") logger.debug(f"Response: {response}")
@@ -65,12 +68,13 @@ async def acreate_chat_completion(
max_tokens: int = None, max_tokens: int = None,
request_timeout: int = 40, request_timeout: int = 40,
num_retries=3, num_retries=3,
on_single_request_timeout: Callable = None on_single_request_timeout: Callable = None,
**kwargs
): ):
"""Create a chat completion using the OpenAI API """Create a chat completion using the OpenAI API
Args: Args:
messages (List[Message]): The messages to send to the chat completion messages (List[dict]): The messages to send to the chat completion
model (str, optional): The model to use. Defaults to None. model (str, optional): The model to use. Defaults to None.
temperature (float, optional): The temperature to use. Defaults to 0.9. temperature (float, optional): The temperature to use. Defaults to 0.9.
max_tokens (int, optional): The max tokens to use. Defaults to None. max_tokens (int, optional): The max tokens to use. Defaults to None.
@@ -100,6 +104,7 @@ async def acreate_chat_completion(
temperature=temperature, temperature=temperature,
max_tokens=max_tokens, max_tokens=max_tokens,
request_timeout=request_timeout, request_timeout=request_timeout,
**kwargs
) )
else: else:
response = await acreate_chat_completion_once( response = await acreate_chat_completion_once(
@@ -108,6 +113,7 @@ async def acreate_chat_completion(
temperature=temperature, temperature=temperature,
max_tokens=max_tokens, max_tokens=max_tokens,
request_timeout=request_timeout, request_timeout=request_timeout,
**kwargs
) )
break break
except RateLimitError: except RateLimitError:
@@ -129,5 +135,10 @@ async def acreate_chat_completion(
if response is None: if response is None:
logger.error(f"Failed to get response from GPT after {num_retries} retries") logger.error(f"Failed to get response from GPT after {num_retries} retries")
raise RuntimeError(f"Failed to get response after {num_retries} retries") raise RuntimeError(f"Failed to get response after {num_retries} retries")
resp = response.choices[0].message["content"]
return resp choice_message = response.choices[0].message
content = choice_message.get("content")
if content is None:
return "function_call", {k: v for k, v in choice_message["function_call"].items()}
else:
return "content", content
+8 -1
View File
@@ -2,6 +2,7 @@
from __future__ import annotations from __future__ import annotations
from typing import List from typing import List
import json
import tiktoken_async import tiktoken_async
@@ -33,7 +34,8 @@ async def count_message_tokens(
elif model == "gpt-4": elif model == "gpt-4":
# !Note: gpt-4 may change over time. Returning num tokens assuming gpt-4-0314.") # !Note: gpt-4 may change over time. Returning num tokens assuming gpt-4-0314.")
return await count_message_tokens(messages, model="gpt-4-0314") return await count_message_tokens(messages, model="gpt-4-0314")
elif model == "gpt-3.5-turbo-0301": # TODO: OpenAI has not mention how to count tokens for 0613, thus, we use the former method
elif model == "gpt-3.5-turbo-0301" or model == "gpt-3.5-turbo-0613" or model == "gpt-3.5-turbo-16k-0613":
tokens_per_message = ( tokens_per_message = (
4 # every message follows <|start|>{role/name}\n{content}<|end|>\n 4 # every message follows <|start|>{role/name}\n{content}<|end|>\n
) )
@@ -51,6 +53,11 @@ async def count_message_tokens(
for message in messages: for message in messages:
num_tokens += tokens_per_message num_tokens += tokens_per_message
for key, value in message.items(): for key, value in message.items():
if not isinstance(value, str):
# TODO: Since openai does not mentioned how to count tokens of 'funciton_call',
# and only string is countable, thus, if the value is not a `str` (`function_call`
# field of a message), we convert it into json
value = json.dumps(value)
num_tokens += len(encoding.encode(value)) num_tokens += len(encoding.encode(value))
if key == "name": if key == "name":
num_tokens += tokens_per_name num_tokens += tokens_per_name
@@ -18,7 +18,18 @@ def reg_or_not():
@functional_module( @functional_module(
name="add_alarm", name="add_alarm",
description="Create an alarm", description="Create an alarm",
signature={"date": "The alarm date, 'YYYY-mm-dd HH:MM:SS format'", "desc": "The event description"}) signature={
"date": {
"description": "The alarm date, 'YYYY-mm-dd HH:MM:SS format'",
"type": "string",
"required": True
},
"desc": {
"description": "The event description",
"type": "string",
"required": True
}
})
async def add_alarm(context: CallerContext, date, desc): async def add_alarm(context: CallerContext, date, desc):
# date = "2023-05-10 14:56:59" # date = "2023-05-10 14:56:59"
now = datetime.datetime.strptime(date, "%Y-%m-%d %H:%M:%S").timestamp() now = datetime.datetime.strptime(date, "%Y-%m-%d %H:%M:%S").timestamp()
@@ -43,7 +54,14 @@ def reg_or_not():
@functional_module( @functional_module(
name="delete_alarm", name="delete_alarm",
description="delete all alarms whose ID is in the list", description="delete all alarms whose ID is in the list",
signature={"IDs": "A list of alarm IDs to"}) signature={
"IDs": {
"type": "array",
"items": { "type": "string" },
"description": "A list of alarm IDs to delete",
"required": True
}
})
async def delete_alarm(context: CallerContext, IDs: List[str]): async def delete_alarm(context: CallerContext, IDs: List[str]):
# get all tasks # get all tasks
result = await do_get(google_calendar_service_address + "/tasks") result = await do_get(google_calendar_service_address + "/tasks")
@@ -113,7 +113,7 @@ Sometimes you maybe asked to generate a pic of myself. That means you MUST add '
- Add unique touches to each output, making it lengthy, detailed, and stylized. - Add unique touches to each output, making it lengthy, detailed, and stylized.
- Show, don't tell; instead of tagging \"exceptional artwork\" or \"emphasizing a beautiful ...\" provide - precise details. - Show, don't tell; instead of tagging \"exceptional artwork\" or \"emphasizing a beautiful ...\" provide - precise details.
- Ensure the output is placed inside a beautiful and stylized markdown. - Ensure the output is placed inside a beautiful and stylized markdown.
- The prompt you return MUST be English. The lenth of prompt MUST less than 150. - The prompt you return MUST be English. The tokens of prompt MUST less than 70.
""" """
OTHER_SD_PARAMS_NAME = "other" OTHER_SD_PARAMS_NAME = "other"
@@ -153,7 +153,7 @@ NOTE: Just reply using these information, don't ask me anything.
sys_prompt = {'role': 'system', 'content': gpt_system_prompt} sys_prompt = {'role': 'system', 'content': gpt_system_prompt}
messages = [sys_prompt, {'role': 'user', 'content': prompt}] messages = [sys_prompt, {'role': 'user', 'content': prompt}]
model = CFG.small_llm_model model = CFG.small_llm_model
resp = await acreate_chat_completion( _, resp = await acreate_chat_completion(
messages, messages,
model, model,
temperature=0, temperature=0,
@@ -207,7 +207,7 @@ NOTE: Just reply using these information, don't ask me anything.
messages = [sys_prompt, {'role': 'user', 'content': "Generation " + origin_str}] messages = [sys_prompt, {'role': 'user', 'content': "Generation " + origin_str}]
model = CFG.small_llm_model model = CFG.small_llm_model
try: try:
resp = await acreate_chat_completion( _, resp = await acreate_chat_completion(
messages, messages,
model, model,
temperature=0, temperature=0,
@@ -239,7 +239,12 @@ NOTE: Just reply using these information, don't ask me anything.
@functional_module( @functional_module(
name="stable_diffusion", name="stable_diffusion",
description="Generate a picture.", description="Generate a picture.",
signature={'prompt': 'the description I told you'}) signature={
'prompt': {
"type": "string",
"description": 'the description I told you'
}
})
async def stable_diffusion(context: CallerContext, prompt: str): async def stable_diffusion(context: CallerContext, prompt: str):
await context.reply_text("I'm generating the image, this may take a while.") await context.reply_text("I'm generating the image, this may take a while.")
style = await determine_style(prompt) style = await determine_style(prompt)
@@ -14,8 +14,12 @@ def reg_or_not():
@functional_module( @functional_module(
name="post_tweet", name="post_tweet",
description="post a tweet", description="post a tweet",
signature={"content": "the content of the tweet"} signature={
) "content": {
"type": "string",
"description": "the content of the tweet"
}
})
async def post_tweet(context: CallerContext, content: str): async def post_tweet(context: CallerContext, content: str):
response = await do_post(twitter_service_address + "/twitter/tweet_post", '', {"content": content}) response = await do_post(twitter_service_address + "/twitter/tweet_post", '', {"content": content})
logger.info(f"response: {response}") logger.info(f"response: {response}")
+24 -4
View File
@@ -14,7 +14,12 @@ def reg_or_not():
@functional_module( @functional_module(
name="youtube_video_brief", name="youtube_video_brief",
description="Get the brief content of a youtube video", description="Get the brief content of a youtube video",
signature={"url": "The address of the video"}) signature={
"url": {
"type": "string",
"description": "The address of the video"
}
})
async def youtube_video_brief(context: CallerContext, url: str): async def youtube_video_brief(context: CallerContext, url: str):
await context.push_notification(f"One second... I'm watching this video: {url}") await context.push_notification(f"One second... I'm watching this video: {url}")
if not url.startswith('https://www.youtube.com/watch?'): if not url.startswith('https://www.youtube.com/watch?'):
@@ -38,7 +43,12 @@ def reg_or_not():
@functional_module( @functional_module(
name="youtube_video_brief_vid", name="youtube_video_brief_vid",
description="Get the brief content of a youtube video identified by video id", description="Get the brief content of a youtube video identified by video id",
signature={"video_id": "The video id of the video"}) signature={
"video_id": {
"type": "string",
"description": "The video id of the video"
}
})
async def youtube_video_brief_vid(context: CallerContext, video_id: str): async def youtube_video_brief_vid(context: CallerContext, video_id: str):
url = f'https://www.youtube.com/watch?v={video_id}' url = f'https://www.youtube.com/watch?v={video_id}'
await context.push_notification(f"One second... I'm watching this video: {url}") await context.push_notification(f"One second... I'm watching this video: {url}")
@@ -70,7 +80,12 @@ def reg_or_not():
@functional_module( @functional_module(
name="youtube_x_video_info", name="youtube_x_video_info",
description="Get the basic information of a youtube user's newest videos, when the summary of videos are not required, you should use this function", description="Get the basic information of a youtube user's newest videos, when the summary of videos are not required, you should use this function",
signature={"username": "The username"}) signature={
"username": {
"type": "string",
"description": "The username"
}
})
async def youtube_x_video_info(context: CallerContext, username: str): async def youtube_x_video_info(context: CallerContext, username: str):
response = await youtube_latest_video_info_of(context, username, False) response = await youtube_latest_video_info_of(context, username, False)
result = f'The brief content of the latest videos of {username} are:\n' result = f'The brief content of the latest videos of {username} are:\n'
@@ -86,7 +101,12 @@ def reg_or_not():
@functional_module( @functional_module(
name="youtube_notify_new", name="youtube_notify_new",
description="Watching an Youtuber, push an notification when the youtuber published a new video", description="Watching an Youtuber, push an notification when the youtuber published a new video",
signature={"username": "The username"}) signature={
"username": {
"type": "string",
"description": "The username"
}
})
async def youtube_notify_new(context: CallerContext, username: str): async def youtube_notify_new(context: CallerContext, username: str):
if username.startswith('@'): if username.startswith('@'):
username = username[1:] username = username[1:]
+10 -1
View File
@@ -4,7 +4,16 @@ from jarvis.functional_modules.functional_module import functional_module, Calle
@functional_module( @functional_module(
name="toggle_light", name="toggle_light",
description="Turn on/off the light.", description="Turn on/off the light.",
signature={"room": "The room name", "on": "Turn on or off, bool type>"}) signature={
"room": {
"type": "string",
"description": "The room name"
},
"on": {
"type": "boolean",
"description": "Turn on or off"
}
})
async def light_switch(context: CallerContext, room: str, on: bool): async def light_switch(context: CallerContext, room: str, on: bool):
# Do the actual control here, something like this # Do the actual control here, something like this
# room_id = convert_room_name_to_id(room) # room_id = convert_room_name_to_id(room)