├── requirements.txt ├── LICENSE ├── README.md ├── .gitignore └── main.py /requirements.txt: -------------------------------------------------------------------------------- 1 | aiohttp==3.8.5 2 | aiosignal==1.3.1 3 | async-timeout==4.0.3 4 | attrs==23.1.0 5 | beautifulsoup4==4.12.2 6 | certifi==2023.7.22 7 | charset-normalizer==3.2.0 8 | frozenlist==1.4.0 9 | googlesearch-python==1.2.3 10 | html2text==2020.1.16 11 | html5lib==1.1 12 | idna==3.4 13 | multidict==6.0.4 14 | openai==0.28.1 15 | python-dotenv==1.0.0 16 | requests==2.31.0 17 | six==1.16.0 18 | soupsieve==2.5 19 | tqdm==4.66.1 20 | urllib3==2.0.5 21 | webencodings==0.5.1 22 | yarl==1.9.2 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Flavio Albano 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Prompt Expander 2 | 3 | ## A Prompt Expander OpenAI-Based. 4 | 5 | ### Introduction 6 | This repository concerns a **Prompt Expander**: a **Proof of Concept Software** that uses **OpenAI API** itself to improve the performance of a **Task** requested in input for a hypothetical **Agent**. 7 | 8 | In my view **an initial expansion of the prompt should be the first task an Agent should perform**. Optimizing the initial prompt (with something Grammarly-like) could also help generate a better response. 9 | 10 | In addition to this, an Agent should extract as much information as possible from the input task such as: **geographical locations**, **ISO 639-1 language codes**, **URLs**, etc. for possible later use. 11 | 12 | --- 13 | 14 | IMPORTANT: using **Markdown** as the format in which to request responses to **prompts** opens up the possibility of using common **regexes** to manage the output from GPT 3.5/4 by taking advantage of the **predictability of the output**. 15 | 16 | --- 17 | 18 | ### The features will: 19 | - Expansion of a task into a number of intermediate steps necessary to carry out the task (implemented) 20 | - Language correction (grammar, etc.) of the prompt (implemented) 21 | - Understand if the task includes a geographical locations and extract it (implemented) 22 | - Understand the language of task and reply with ISO 639-1 language code (implemented) 23 | - Understand if the task includes a URL and extract it (implemented) 24 | - Understand whether a Google search is necessary to carry out a step and, if the answer is positive, carry it out (partially implemented) 25 | - Understand whether scraping from a web page is necessary to carry out a step (partially implemented) 26 | - Saving detailed logs to disk (in Markdown format), necessary later to reconstruct the inputs/prompts/outputs chain (to do) 27 | - Use of Markdown as a standard for processing (both for **predictability of OpenAI GPT 3.5/4 output** and for **simplicity of manage** the format itself with common regexes) 28 | 29 | 30 | If you want more insight you could read this post blog: https://ingegnerealbano.com/prompt-expansion-con-openai-potrebbe-essere-una-nuova-idea/ 31 | 32 | --- 33 | 34 | ### Dependencies 35 | - pip install --upgrade pip 36 | - pip install --upgrade openai 37 | - pip install --upgrade googlesearch-python 38 | - pip install --upgrade beautifulsoup4 39 | - pip install --upgrade html5lib 40 | - pip install --upgrade html2text 41 | - pip install --upgrade python-dotenv 42 | 43 | But requirements.txt is provided. 44 | 45 | --- 46 | 47 | ### To Run Just 48 | 1) clone the repository: git clone ... 49 | 2) create and activate a venv 50 | 3) install dependencies or use requirements.txt 51 | 4) put a .env file with your OpenAI API Key into the project root 52 | 5) python3 main.py 53 | 6) enjoy? ;) 54 | 55 | --- 56 | 57 | #### Please note that no milestones are provided and no guarantees that this software will be completed. 58 | 59 | --- 60 | 61 | ## Please note that, currently, this software has to be slowed down because it requires more than 10,000 tokens per minute... 62 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # poetry 98 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 99 | # This is especially recommended for binary packages to ensure reproducibility, and is more 100 | # commonly ignored for libraries. 101 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 102 | #poetry.lock 103 | 104 | # pdm 105 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 106 | #pdm.lock 107 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 108 | # in version control. 109 | # https://pdm.fming.dev/#use-with-ide 110 | .pdm.toml 111 | 112 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 113 | __pypackages__/ 114 | 115 | # Celery stuff 116 | celerybeat-schedule 117 | celerybeat.pid 118 | 119 | # SageMath parsed files 120 | *.sage.py 121 | 122 | # Environments 123 | .env 124 | .venv 125 | env/ 126 | venv/ 127 | ENV/ 128 | env.bak/ 129 | venv.bak/ 130 | 131 | # Spyder project settings 132 | .spyderproject 133 | .spyproject 134 | 135 | # Rope project settings 136 | .ropeproject 137 | 138 | # mkdocs documentation 139 | /site 140 | 141 | # mypy 142 | .mypy_cache/ 143 | .dmypy.json 144 | dmypy.json 145 | 146 | # Pyre type checker 147 | .pyre/ 148 | 149 | # pytype static type analyzer 150 | .pytype/ 151 | 152 | # Cython debug symbols 153 | cython_debug/ 154 | 155 | # PyCharm 156 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 157 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 158 | # and can be added to the global gitignore or merged into this file. For a more nuclear 159 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 160 | #.idea/ 161 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | # Import Stuffs 2 | import os 3 | import re 4 | import openai 5 | import requests 6 | import html2text 7 | import time 8 | from googlesearch import search 9 | from bs4 import BeautifulSoup 10 | from dotenv import load_dotenv 11 | 12 | # Take environment variables from .env file 13 | load_dotenv() 14 | 15 | # Set OpenAI API KEY 16 | openai.api_key = os.getenv("OPENAI_API_KEY") 17 | 18 | 19 | def reply_boolean_or_none_to_assertion(assertion): 20 | if assertion.lower() == "yes": 21 | return True 22 | elif assertion.lower() == "no": 23 | return False 24 | else: 25 | return None 26 | 27 | 28 | def basilar_query_to_openai(prompt, model="gpt-4-1106-preview", temperature=0.6, max_tokens=4000, top_p=1, frequency_penalty=0, presence_penalty=0): 29 | response = openai.ChatCompletion.create( 30 | model=model, 31 | messages=prompt, 32 | temperature=temperature, 33 | max_tokens=max_tokens, 34 | top_p=top_p, 35 | frequency_penalty=frequency_penalty, 36 | presence_penalty=presence_penalty 37 | ) 38 | 39 | return response["choices"][0]["message"]["content"] 40 | 41 | 42 | def is_the_prompt_correct(prompt, model="gpt-4-1106-preview", temperature=0.5, max_tokens=2048, top_p=1, frequency_penalty=0, presence_penalty=0): 43 | prompt = "Does this text need to be corrected semantically or syntactically? Answer exclusively with yes or no.\n" + prompt 44 | 45 | prompt = [ 46 | { 47 | "role": "user", 48 | "content": prompt 49 | } 50 | ] 51 | 52 | response = openai.ChatCompletion.create( 53 | model=model, 54 | messages=prompt, 55 | temperature=temperature, 56 | max_tokens=max_tokens, 57 | top_p=top_p, 58 | frequency_penalty=frequency_penalty, 59 | presence_penalty=presence_penalty 60 | ) 61 | 62 | return response["choices"][0]["message"]["content"] 63 | 64 | 65 | def prompt_corrector(prompt, model="gpt-4-1106-preview", temperature=0.6, max_tokens=4096, top_p=1, frequency_penalty=0, presence_penalty=0): 66 | prompt = "Correct semantically and syntactically this text: " + prompt 67 | 68 | prompt = [ 69 | { 70 | "role": "user", 71 | "content": prompt 72 | } 73 | ] 74 | 75 | response = openai.ChatCompletion.create( 76 | model=model, 77 | messages=prompt, 78 | temperature=temperature, 79 | max_tokens=max_tokens, 80 | top_p=top_p, 81 | frequency_penalty=frequency_penalty, 82 | presence_penalty=presence_penalty 83 | ) 84 | 85 | return response["choices"][0]["message"]["content"] 86 | 87 | 88 | def is_it_geolocalizable(prompt, model="gpt-4-1106-preview", temperature=0.5, max_tokens=2048, top_p=1, frequency_penalty=0, presence_penalty=0): 89 | prompt = "Does this text include a geographic location? Answer exclusively with yes or no.\n" + prompt 90 | 91 | prompt = [ 92 | { 93 | "role": "user", 94 | "content": prompt 95 | } 96 | ] 97 | 98 | response = openai.ChatCompletion.create( 99 | model=model, 100 | messages=prompt, 101 | temperature=temperature, 102 | max_tokens=max_tokens, 103 | top_p=top_p, 104 | frequency_penalty=frequency_penalty, 105 | presence_penalty=presence_penalty 106 | ) 107 | 108 | return response["choices"][0]["message"]["content"] 109 | 110 | 111 | def geolocalize(prompt, model="gpt-4-1106-preview", temperature=0.5, max_tokens=2048, top_p=1, frequency_penalty=0, presence_penalty=0): 112 | prompt = "To which geographic location does the following text refer? Reply only with a geographic location." + prompt 113 | 114 | prompt = [ 115 | { 116 | "role": "user", 117 | "content": prompt 118 | } 119 | ] 120 | 121 | response = openai.ChatCompletion.create( 122 | model=model, 123 | messages=prompt, 124 | temperature=temperature, 125 | max_tokens=max_tokens, 126 | top_p=top_p, 127 | frequency_penalty=frequency_penalty, 128 | presence_penalty=presence_penalty 129 | ) 130 | 131 | return response["choices"][0]["message"]["content"] 132 | 133 | 134 | def need_search_on_google(prompt, model="gpt-4-1106-preview", temperature=0.5, max_tokens=2048, top_p=1, frequency_penalty=0, presence_penalty=0): 135 | prompt = "Do I need to do a Google search to do this? Answer exclusively with yes or no.\n" + prompt 136 | 137 | prompt = [ 138 | { 139 | "role": "user", 140 | "content": prompt 141 | } 142 | ] 143 | 144 | response = openai.ChatCompletion.create( 145 | model=model, 146 | messages=prompt, 147 | temperature=temperature, 148 | max_tokens=max_tokens, 149 | top_p=top_p, 150 | frequency_penalty=frequency_penalty, 151 | presence_penalty=presence_penalty 152 | ) 153 | 154 | return response["choices"][0]["message"]["content"] 155 | 156 | 157 | def need_scraping_on_web(prompt, model="gpt-4-1106-preview", temperature=0.5, max_tokens=2048, top_p=1, frequency_penalty=0, presence_penalty=0): 158 | prompt = "Do I need to do scraping on the web to do this? Answer exclusively with yes or no.\n" + prompt 159 | 160 | prompt = [ 161 | { 162 | "role": "user", 163 | "content": prompt 164 | } 165 | ] 166 | 167 | response = openai.ChatCompletion.create( 168 | model=model, 169 | messages=prompt, 170 | temperature=temperature, 171 | max_tokens=max_tokens, 172 | top_p=top_p, 173 | frequency_penalty=frequency_penalty, 174 | presence_penalty=presence_penalty 175 | ) 176 | 177 | return response["choices"][0]["message"]["content"] 178 | 179 | 180 | def what_language_is_it_written_in(prompt, model="gpt-4-1106-preview", temperature=0.5, max_tokens=2048, top_p=1, frequency_penalty=0, presence_penalty=0): 181 | prompt = "In what language is the following text? Reply exclusively with an ISO 639-1 code.\n" + prompt 182 | 183 | prompt = [ 184 | { 185 | "role": "user", 186 | "content": prompt 187 | } 188 | ] 189 | 190 | response = openai.ChatCompletion.create( 191 | model=model, 192 | messages=prompt, 193 | temperature=temperature, 194 | max_tokens=max_tokens, 195 | top_p=top_p, 196 | frequency_penalty=frequency_penalty, 197 | presence_penalty=presence_penalty 198 | ) 199 | 200 | return response["choices"][0]["message"]["content"].lower() 201 | 202 | 203 | def it_contains_url(prompt, model="gpt-4-1106-preview", temperature=0.5, max_tokens=2048, top_p=1, frequency_penalty=0, presence_penalty=0): 204 | prompt = "Does this text contain a URL? Reply exclusively with yes or no.\n" + prompt 205 | 206 | prompt = [ 207 | { 208 | "role": "user", 209 | "content": prompt 210 | } 211 | ] 212 | 213 | response = openai.ChatCompletion.create( 214 | model=model, 215 | messages=prompt, 216 | temperature=temperature, 217 | max_tokens=max_tokens, 218 | top_p=top_p, 219 | frequency_penalty=frequency_penalty, 220 | presence_penalty=presence_penalty 221 | ) 222 | 223 | return response["choices"][0]["message"]["content"] 224 | 225 | 226 | def contains_url(prompt, model="gpt-4-1106-preview", temperature=0.5, max_tokens=2048, top_p=1, frequency_penalty=0, presence_penalty=0): 227 | prompt = "Extract the URL contained in this text; reply to this query with a URL only.\n" + prompt 228 | 229 | prompt = [ 230 | { 231 | "role": "user", 232 | "content": prompt 233 | } 234 | ] 235 | 236 | response = openai.ChatCompletion.create( 237 | model=model, 238 | messages=prompt, 239 | temperature=temperature, 240 | max_tokens=max_tokens, 241 | top_p=top_p, 242 | frequency_penalty=frequency_penalty, 243 | presence_penalty=presence_penalty 244 | ) 245 | 246 | return response["choices"][0]["message"]["content"] 247 | 248 | 249 | def search_google(query, query_language): 250 | try: 251 | # Search on Google 252 | results = search(query, num_results=10, advanced=True, lang=query_language) 253 | return results 254 | 255 | except Exception as e: 256 | return False 257 | 258 | 259 | def extract_text_from_html_page(url): 260 | # Request to webpage 261 | response = requests.get(url) 262 | 263 | # Create a BeautifulSoup object to parse the HTML of the page 264 | soup = BeautifulSoup(response.text, "html5lib") 265 | 266 | # Use html2text to convert HTML to Markdown 267 | text_maker = html2text.HTML2Text() 268 | # text_maker.ignore_links = True 269 | 270 | markdown_text = text_maker.handle(soup.prettify()) 271 | 272 | return markdown_text 273 | 274 | 275 | ### The script starts here ### 276 | # Input the task 277 | task = input("Please enter the task to be performed: ") 278 | print("---") 279 | 280 | # Examine if input task is semantically and syntactically correct 281 | need_corrections = is_the_prompt_correct(task) 282 | need_corrections_boolean = reply_boolean_or_none_to_assertion(need_corrections) 283 | 284 | # Debug print 285 | print("Does this text need to be corrected semantically or syntactically? " + need_corrections) 286 | 287 | 288 | if need_corrections_boolean: 289 | task = prompt_corrector(task) 290 | 291 | # Debug Print 292 | print("The task after correction is: " + task) 293 | 294 | print("---") 295 | 296 | 297 | the_prompt_is_geolocalizable = is_it_geolocalizable(task) 298 | 299 | # Debug Print 300 | print("Does the task talk about a geographic location? " + the_prompt_is_geolocalizable) 301 | 302 | 303 | if reply_boolean_or_none_to_assertion(the_prompt_is_geolocalizable): 304 | place = geolocalize(task) 305 | 306 | # Debug Print 307 | print("The geographic location in the task is: " + place) 308 | print("---") 309 | 310 | 311 | # Debug Print 312 | # print("Now wait 61 seconds for avoid exceeding 10,000 tokens/min") 313 | print("---") 314 | 315 | # To avoid exceeding 10,000 tokens/min 316 | # time.sleep(61) 317 | 318 | 319 | the_prompt_contain_url = it_contains_url(task) 320 | 321 | # Debug Print 322 | print("Does the prompt contains URL? " + the_prompt_contain_url) 323 | 324 | 325 | if reply_boolean_or_none_to_assertion(the_prompt_contain_url): 326 | url = contains_url(task) 327 | 328 | # Debug Print 329 | print("The URL in the task is: " + url) 330 | print("---") 331 | 332 | 333 | # Debug Print 334 | # print("Now wait 61 seconds for avoid exceeding 10,000 tokens/min") 335 | print("---") 336 | 337 | # To avoid exceeding 10,000 tokens/min 338 | # time.sleep(61) 339 | 340 | 341 | # Extract language from task in ISO 639-1 code 342 | language = what_language_is_it_written_in(task) 343 | 344 | # Debug Print 345 | print("ISO 639-1 language code of the task: " + language) 346 | print("---") 347 | 348 | 349 | # Build the first prompt expansion 350 | history = [ 351 | { 352 | "role": "user", 353 | "content": "Task to perform: " + task 354 | }, 355 | { 356 | "role": "assistant", 357 | "content": "Decide how many steps are needed to accomplish the task and list them in a numbered list. The list must consisting of one line for each step; format the response at this query in markdown." 358 | } 359 | ] 360 | 361 | 362 | # Debug Print 363 | # print("Now wait 61 seconds for avoid exceeding 10,000 tokens/min") 364 | print("---") 365 | 366 | # To avoid exceeding 10,000 tokens/min 367 | # time.sleep(61) 368 | 369 | 370 | # First query to OpenAI 371 | first_step_response = basilar_query_to_openai(history) 372 | 373 | # Debug Print 374 | print("First step raw response from OpenAI") 375 | print(first_step_response) 376 | print("---") 377 | 378 | 379 | # Define the RegExes 380 | # To extract the steps from numbered list in markdown - It may cause problems and not capture the query output correctly sometimes 381 | numbered_list_regex = r"\d+\.\s(.+)\n+" 382 | # To extract the points from bulleted list in markdown - It may cause problems and not capture the query output correctly sometimes 383 | bulleted_list_regex = r"-\s(.+)\n+" 384 | 385 | # Extract the steps from numbered list in markdown 386 | steps = re.findall(numbered_list_regex, first_step_response + "\n") 387 | 388 | # Initialize some variables 389 | dictionary_step = dict() 390 | list_steps = list() 391 | step_number = 1 392 | 393 | # Debug Print 394 | print(steps) 395 | print(type(steps)) 396 | 397 | # Fill a list with the step each in a dictionary 398 | for step in steps: 399 | # Debug Print 400 | # print("Now wait 61 seconds for avoid exceeding 10,000 tokens/min") 401 | print("---") 402 | 403 | # To avoid exceeding 10,000 tokens/min 404 | # time.sleep(61) 405 | 406 | # Each step in a dictionary 407 | dictionary_step = { 408 | "step_number": step_number, 409 | "step_for_task": step, 410 | "need_search_on_google": reply_boolean_or_none_to_assertion(need_search_on_google(step)), 411 | "need_scraping_on_web": reply_boolean_or_none_to_assertion(need_scraping_on_web(step)), 412 | "contains_geographic_location": reply_boolean_or_none_to_assertion(is_it_geolocalizable(step)), 413 | "contains_url": reply_boolean_or_none_to_assertion(it_contains_url(step)) 414 | } 415 | 416 | step_number += 1 417 | 418 | # Debug Print 419 | print(dictionary_step) 420 | print("---") 421 | 422 | # Add the dictionary to the list 423 | list_steps.append(dictionary_step) 424 | 425 | # To be continued... 426 | --------------------------------------------------------------------------------