├── example.jpg ├── browsing.py ├── functions.py └── .gitignore /example.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jfloresy/chatgpt-browsing/HEAD/example.jpg -------------------------------------------------------------------------------- /browsing.py: -------------------------------------------------------------------------------- 1 | import time 2 | from urllib.parse import urlparse 3 | 4 | import openai 5 | import requests 6 | from bs4 import BeautifulSoup 7 | 8 | from functions import Functions, Param 9 | 10 | browsing = Functions() 11 | 12 | openai.api_key = "sk-" 13 | 14 | hrefs = [] 15 | 16 | 17 | @browsing 18 | def browse( 19 | url: Param(str, "URL") 20 | ): 21 | """Open URL and get site text and available links""" 22 | html = requests.get(url).text 23 | soup = BeautifulSoup(html, 'html.parser') 24 | parse = urlparse(url) 25 | 26 | print(f"$ Open {url}") 27 | 28 | links = [] 29 | for n, a in enumerate(soup.find_all('a', href=True)): 30 | link = a['href'] 31 | if link.startswith("/"): 32 | link = f"{parse.scheme}://{parse.netloc}{link}" 33 | hrefs.append(link) 34 | links.append(f"Link №{len(hrefs)} - {a.text}") 35 | 36 | return ", ".join(links) + "\n" + soup.text 37 | 38 | 39 | @browsing 40 | def google( 41 | query: Param(str, "Search query") 42 | ): 43 | """Open google search""" 44 | return browse("https://www.google.com/search?q=" + query) 45 | 46 | 47 | @browsing 48 | def click_link( 49 | n: Param(int, "Link №") 50 | ): 51 | """Click on link""" 52 | print(f"$ Click on №{n}") 53 | return browse(hrefs[n]) 54 | 55 | 56 | messages = [] 57 | 58 | while True: 59 | messages.append({ 60 | "role": "user", 61 | "content": input("> ") 62 | }) 63 | 64 | response = openai.ChatCompletion.create( 65 | model="gpt-3.5-turbo-0613", 66 | messages=messages, 67 | functions=browsing.to_json(), 68 | function_call="auto", 69 | ) 70 | 71 | message = response["choices"][0]["message"] 72 | messages.append(message) 73 | result = browsing.call_functions(message) 74 | 75 | if result: 76 | messages.append(result) 77 | 78 | while result is not None: 79 | last_request_time = time.time() 80 | response = openai.ChatCompletion.create( 81 | model="gpt-3.5-turbo-16k-0613", 82 | messages=messages, 83 | functions=browsing.to_json(), 84 | function_call="auto", 85 | ) 86 | message = response["choices"][0]["message"] 87 | result = browsing.call_functions(message) 88 | if result: 89 | messages.append(result) 90 | request_sleep_time = 15 - (time.time() - last_request_time) 91 | if request_sleep_time > 0: 92 | time.sleep(request_sleep_time) 93 | 94 | print(message["content"]) 95 | -------------------------------------------------------------------------------- /functions.py: -------------------------------------------------------------------------------- 1 | """ 2 | By Dmitry Kotov t.me/wavecat 3 | """ 4 | 5 | import json 6 | from dataclasses import dataclass 7 | from typing import Callable, Optional 8 | 9 | 10 | @dataclass 11 | class Param: 12 | type: type 13 | description: str 14 | enum: list[object] = None 15 | required: bool = True 16 | 17 | 18 | types = { 19 | "str": "string", 20 | "int": "integer" 21 | } 22 | 23 | 24 | class Functions: 25 | functions = {} 26 | 27 | def __call__(self, function: Callable): 28 | self.functions[function.__name__] = function 29 | return function 30 | 31 | def to_json(self): 32 | result = [] 33 | 34 | for function_name, function in self.functions.items(): 35 | args = function.__annotations__.copy() 36 | 37 | properties = {} 38 | result.append({ 39 | "name": function_name, 40 | "description": function.__doc__, 41 | "parameters": { 42 | "type": "object", 43 | "properties": properties 44 | }, 45 | "required": [] 46 | }) 47 | 48 | for name, arg in args.items(): 49 | if isinstance(arg, Param): 50 | properties[name] = { 51 | "type": types.get(arg.type.__name__, "string") 52 | } 53 | 54 | if arg.description: 55 | properties[name]["description"] = arg.description 56 | 57 | if arg.enum: 58 | properties[name]["enum"] = arg.enum 59 | 60 | if arg.required: 61 | result[-1]["required"].append(name) 62 | 63 | return result 64 | 65 | def call_functions(self, message: dict) -> Optional[dict]: 66 | function_call = message.get("function_call") 67 | 68 | if function_call: 69 | function_name = function_call["name"] 70 | 71 | if self.functions.get(function_name): 72 | function = self.functions[function_name] 73 | 74 | arguments = json.loads(function_call["arguments"]) 75 | 76 | call_args = {} 77 | for name in function.__annotations__.keys(): 78 | arg = arguments.get(name) 79 | if arg: 80 | call_args[name] = arg 81 | 82 | result = function(**call_args) 83 | 84 | return { 85 | "role": "function", 86 | "name": function_name, 87 | "content": str(result), 88 | } 89 | 90 | return None 91 | -------------------------------------------------------------------------------- /.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 | --------------------------------------------------------------------------------