11from dataclasses import dataclass , field
2+ from typing import Any , Dict , List
23from api .classes import Agent , AvailableActions , Action , Observation , Rules
3- import random
44import openai
55import api .util as util
66import ast
77import json
8+ from PIL import Image
89
910
1011action_format_instructions_no_openended = """\
3031
3132@dataclass
3233class OpenAITextAgent (Agent ):
33- openai_model : str
34- agent_type_id : str
35- system_message : str = "You are an agent playing a game. Select the action that maximizes your probability of winning."
36- max_retries : int = 3
37- transparent_reasoning : bool = False
34+ openai_model : str
35+ agent_type_id : str
36+ system_message : str = "You are an agent playing a game. Select the action that maximizes your probability of winning."
37+ max_retries : int = 3
38+ transparent_reasoning : bool = False
3839 mode : int = 0 # 0 = normal, 1 = chain of thought, 2 = babble and prune
39-
40+
4041 def print (self , * args , ** kwargs ):
4142 if self .transparent_reasoning :
4243 print (self .agent_type_id , * args , ** kwargs )
44+
45+ def get_user_message_content (self , text_prompt : str , image : Image ) -> List [Dict [str , Any ]]:
46+ return [
47+ {
48+ "type" : "text" ,
49+ "text" : text_prompt
50+ }
51+ ]
4352
44- def take_action (
45- self ,
46- rules : Rules ,
47- observation : Observation ,
48- available_actions : AvailableActions ,
49- show_state : bool ,
50- ):
53+ def get_request_params (self , messages : List [Dict [str , Any ]]) -> Dict [str , Any ]:
54+ return {
55+ "model" : self .openai_model ,
56+ "messages" : messages ,
57+ "response_format" : { "type" : "json_object" }
58+ }
59+
60+ def take_action (self , rules : Rules , observation : Observation , available_actions : AvailableActions , show_state : bool ) -> Action :
5161 valid_actions = []
5262 prompt = f"You are playing a game called { rules .title } . The rules are as follows:\n { rules .summary } \n "
5363 if rules .additional_details != None :
5464 prompt += "The following are headings with additional information about the rules that you can expand by taking the action Explain(<heading key>).\n "
55- details_dict = {f"H{ i + 1 } " : topic for i , topic in enumerate (rules .additional_details )}
65+ details_dict = {f"H{ i + 1 } " : topic + " - " + description for i , ( topic , description ) in enumerate (rules .additional_details . items () )}
5666 prompt += json .dumps (details_dict , indent = 4 )
5767 valid_actions .extend (f"Explain({ h } )" for h in list (details_dict .keys ()))
5868
@@ -83,7 +93,10 @@ def take_action(
8393 ):
8494 prompt += "Return the action Explain(<action>) to receive additional info about what any of the above actions do.\n "
8595
86- messages = [{"role" : "system" , "content" : self .system_message }]
96+ messages = [
97+ {"role" : "system" , "content" : self .system_message },
98+ {"role" : "user" , "content" : self .get_user_message_content (prompt , observation .image )},
99+ ]
87100
88101 # Chain of Thought
89102 if self .mode == 1 :
@@ -117,7 +130,7 @@ def take_action(
117130 )
118131 messages .append ({"role" : "assistant" , "content" : response })
119132 prompt = ""
120-
133+
121134 self .print (
122135 f"GPT listed the following actions as possibilities: { response } "
123136 )
@@ -128,22 +141,21 @@ def take_action(
128141 prompt += str (list (available_actions .openended ))
129142 messages .append ({"role" : "user" , "content" : prompt })
130143
144+
145+
146+ prompt += "\n Make sure to return ONLY a JSON. It should contain an 'action' key which contains one of the valid actions. And nothing outside the curly braces of the JSON."
147+ #prompt += str(list(valid_actions))
148+
149+ #print(prompt)
131150 result = None
151+
132152 for _ in range (self .max_retries ):
133- response = (
134- openai_client .chat .completions .create (
135- model = self .openai_model ,
136- response_format = {"type" : "json_object" },
137- messages = messages ,
138- )
139- .choices [0 ]
140- .message .content
141- )
153+ response = openai_client .chat .completions .create (** self .get_request_params (messages )).choices [0 ].message .content
142154 messages .append ({"role" : "assistant" , "content" : response })
143155 self .print ("GPT responded with" , response )
144156
145157 try :
146- action = ast .literal_eval (response )
158+ action = ast .literal_eval (util . extract_json ( response ) )
147159 except :
148160 self .print ("GPT returned invalid JSON" )
149161 continue
@@ -154,27 +166,23 @@ def take_action(
154166 messages .append ({"role" : "user" , "content" : error_message })
155167 continue
156168
169+
157170 if action ["action" ] in valid_actions :
158171 self .print ("GPT chose valid action" , action )
159172 result = action
160173 break
161-
174+
162175 self .print ("GPT returned invalid action" , action )
163176 error_message = f"{ action ['action' ]} is not one of the valid actions. "
164177 error_message += "As a reminder, the valid actions are as follows:\n "
165178 error_message += f"{ str (list (valid_actions ))} \n "
166179 error_message += "Please return a json with the key 'action' with the action you choose and (optionally) the key 'openended_response' if you select openended response action."
167180 messages .append ({"role" : "user" , "content" : error_message })
181+
168182 if result == None :
169- self .print (
170- f"WARNING: GPT returned an a random action after { self .max_retries } tries"
171- )
183+ self .print (f"WARNING: GPT returned an a random action after { self .max_retries } tries" )
172184 return Action (action_id = None )
173- return Action (
174- action_id = result ["action" ],
175- openended_response = result .get ("openended_response" ),
176- )
177-
185+ return Action (action_id = result ["action" ], openended_response = result .get ("openended_response" ))
178186
179187@dataclass
180188class ChatGPTText (OpenAITextAgent ):
@@ -199,3 +207,30 @@ class BabbleAndPrune(OpenAITextAgent):
199207 openai_model : str = "gpt-4-1106-preview"
200208 agent_type_id : str = "b&p"
201209 mode : int = 2
210+
211+ @dataclass
212+ class GPT4Vision (OpenAITextAgent ):
213+ openai_model : str = "gpt-4-vision-preview"
214+ agent_type_id : str = "gpt-4-vision"
215+ is_vision_agent : bool = True
216+
217+ def get_user_message_content (self , text_prompt : str , image : Image ) -> List [Dict [str , str ]]:
218+ content = super ().get_user_message_content (text_prompt , image )
219+ if image is not None :
220+ content .append ({
221+ "type" : "image_url" ,
222+ "image_url" : {
223+ "url" : f"data:image/png;base64,{ util .base64_encode_image (image )} " ,
224+ "detail" : "low"
225+ }
226+ })
227+ return content
228+
229+ def get_request_params (self , messages : List [Dict [str , Any ]]) -> Dict [str , Any ]:
230+ return {
231+ "model" : self .openai_model ,
232+ "messages" : messages ,
233+ # As vision models have a low(but undocumented?) default value for below parameter
234+ # https://community.openai.com/t/documented-max-token-default-is-incorrect-for-gpt-4-vision-preview/507329
235+ "max_tokens" : 250 ,
236+ }
0 commit comments