├── .gitattributes ├── .github └── workflows │ └── pylint.yml ├── .gitignore ├── LICENSE ├── README.md ├── docs ├── images │ ├── demo.png │ └── openmacro-title.svg └── videos │ └── demo.mov ├── openmacro ├── __init__.py ├── __main__.py ├── cli.py ├── computer │ └── __init__.py ├── core │ ├── __init__.py │ └── prompts │ │ ├── conversational.txt │ │ ├── initial.txt │ │ ├── instructions.txt │ │ └── memorise.txt ├── extensions │ ├── __init__.py │ ├── browser │ │ ├── __init__.py │ │ ├── config.default.toml │ │ ├── docs │ │ │ └── instructions.md │ │ ├── src │ │ │ ├── engines.json │ │ │ └── user_agents.txt │ │ ├── tests │ │ │ └── tests.py │ │ └── utils │ │ │ ├── general.py │ │ │ └── google.py │ ├── email │ │ ├── __init__.py │ │ └── docs │ │ │ └── instructions.md │ └── extensions.txt ├── llm │ ├── __init__.py │ └── models │ │ └── samba.py ├── memory │ ├── __init__.py │ ├── client.py │ └── server.py ├── omi │ └── __init__.py ├── profile │ ├── __init__.py │ └── template.py ├── speech │ ├── __init__.py │ ├── stt.py │ └── tts.py ├── tests │ ├── __init__.py │ └── cases │ │ └── browser.py └── utils │ └── __init__.py ├── poetry.lock └── pyproject.toml /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.github/workflows/pylint.yml: -------------------------------------------------------------------------------- 1 | name: Pylint 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | strategy: 9 | matrix: 10 | python-version: ["3.8", "3.9", "3.10"] 11 | steps: 12 | - uses: actions/checkout@v4 13 | - name: Set up Python ${{ matrix.python-version }} 14 | uses: actions/setup-python@v3 15 | with: 16 | python-version: ${{ matrix.python-version }} 17 | - name: Install dependencies 18 | run: | 19 | python -m pip install --upgrade pip 20 | pip install pylint 21 | - name: Analysing the code with pylint 22 | run: | 23 | pylint $(git ls-files '*.py') 24 | -------------------------------------------------------------------------------- /.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 | config.toml 11 | .Python 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | wheels/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | cover/ 54 | 55 | # Translations 56 | *.mo 57 | *.pot 58 | 59 | # Django stuff: 60 | *.log 61 | local_settings.py 62 | db.sqlite3 63 | db.sqlite3-journal 64 | 65 | # ChromaDB stuff: 66 | chroma.sqlite3 67 | 68 | # Openmacro stuff: 69 | profiles/ 70 | 71 | # Flask stuff: 72 | instance/ 73 | .webassets-cache 74 | 75 | # Scrapy stuff: 76 | .scrapy 77 | 78 | # Sphinx documentation 79 | docs/_build/ 80 | 81 | # PyBuilder 82 | .pybuilder/ 83 | target/ 84 | 85 | # Jupyter Notebook 86 | .ipynb_checkpoints 87 | 88 | # IPython 89 | profile_default/ 90 | ipython_config.py 91 | 92 | # pyenv 93 | # For a library or package, you might want to ignore these files since the code is 94 | # intended to run in multiple environments; otherwise, check them in: 95 | # .python-version 96 | 97 | # pipenv 98 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 99 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 100 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 101 | # install all needed dependencies. 102 | #Pipfile.lock 103 | 104 | # poetry 105 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 106 | # This is especially recommended for binary packages to ensure reproducibility, and is more 107 | # commonly ignored for libraries. 108 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 109 | #poetry.lock 110 | 111 | # pdm 112 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 113 | #pdm.lock 114 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 115 | # in version control. 116 | # https://pdm.fming.dev/#use-with-ide 117 | .pdm.toml 118 | 119 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 120 | __pypackages__/ 121 | 122 | # Celery stuff 123 | celerybeat-schedule 124 | celerybeat.pid 125 | 126 | # SageMath parsed files 127 | *.sage.py 128 | 129 | # Environments 130 | .env 131 | .venv 132 | env/ 133 | venv/ 134 | ENV/ 135 | env.bak/ 136 | venv.bak/ 137 | 138 | # Spyder project settings 139 | .spyderproject 140 | .spyproject 141 | 142 | # Rope project settings 143 | .ropeproject 144 | 145 | # mkdocs documentation 146 | /site 147 | 148 | # mypy 149 | .mypy_cache/ 150 | .dmypy.json 151 | dmypy.json 152 | 153 | # Pyre type checker 154 | .pyre/ 155 | 156 | # pytype static type analyzer 157 | .pytype/ 158 | 159 | # Cython debug symbols 160 | cython_debug/ 161 | 162 | # PyCharm 163 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 164 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 165 | # and can be added to the global gitignore or merged into this file. For a more nuclear 166 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 167 | #.idea/ 168 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 amoooooo 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 |
2 | 3 | openmacro 4 | 5 |
6 |
7 | License 8 | Commit Activity 9 | Last Commit 10 | Downloads 11 | Stars 12 |
13 | 14 | ## 15 | 16 | https://github.com/user-attachments/assets/9360dfeb-a471-49c3-bbdc-72b32cc8eaeb 17 | 18 | > [!WARNING] 19 | > DISCLAIMER: Project is in its early stage of development. Current version is not stable. 20 | 21 | openmacro is a multimodal personal agent that allows LLMs to run code locally. openmacro aims to act as a personal agent capable of completing and automating simple to complex tasks autonomously via self prompting. 22 | 23 | This provides a cli natural-language interface for you to: 24 | 25 | + Complete and automate simple to complex tasks. 26 | + Analyse and plot data. 27 | + Browse the web for the latest information. 28 | + Manipulate files including photos, videos, PDFs, etc. 29 | 30 | At the moment, openmacro only supports API keys for models powered by SambaNova. Why? Because it’s free, fast, and reliable, which makes it ideal for testing as the project grows! Support for other hosts such as OpenAI and Anthropic is planned to be added in future versions. 31 | 32 | This project is heavily inspired by [`Open Interpreter`](https://github.com/OpenInterpreter/open-interpreter) ❤️ 33 | 34 | ## Quick Start 35 | To get started with openmacro, get a free API key by creating an account at [https://cloud.sambanova.ai/](https://cloud.sambanova.ai/). 36 | 37 | Next, install and start openmacro by running: 38 | ```shell 39 | pip install openmacro 40 | macro --api_key "YOUR_API_KEY" 41 | ``` 42 | > [!TIP] 43 | > Not working? Raise an issue [here](https://github.com/amooo-ooo/openmacro/issues/new) or try this out instead: 44 | ```shell 45 | py -m pip install openmacro 46 | py -m openmacro --api_key "YOUR_API_KEY" 47 | ``` 48 | > [!NOTE] 49 | > You only need to pass `--api_key` once! Next time simply call `macro` or `py -m openmacro`. 50 | 51 | > [!TIP] 52 | > You can also assign different api-keys to different profiles! 53 | ```shell 54 | py -m openmacro --api_key "YOUR_API_KEY" --profile "path\to\profile" 55 | ``` 56 | 57 | ## Profiles 58 | openmacro supports cli args and customised settings! You can view arg options by running: 59 | ```shell 60 | macro --help 61 | ``` 62 | To add your own personalised settings and save it for the future, run: 63 | ```shell 64 | macro --profile "path\to\profile" 65 | ``` 66 | Openmacro supports custom profiles in `JSON`, `TOML`, `YAML` and `Python`: 67 | 68 |
69 | Python 70 | Profiles in `python` allow direct customisation and type safety! 71 | 72 | What your `profile.py` might look like: 73 | ```python 74 | # imports 75 | from openmacro.profile import Profile 76 | from openmacro.extensions import BrowserKwargs, EmailKwargs 77 | 78 | # profile setup 79 | profile: Profile = Profile( 80 | user = { 81 | "name": "Amor", 82 | "version": "1.0.0" 83 | }, 84 | assistant = { 85 | "name": "Macro", 86 | "personality": "You respond in a professional attitude and respond in a formal, yet casual manner.", 87 | "messages": [], 88 | "breakers": ["the task is done.", 89 | "the conversation is done."] 90 | }, 91 | safeguards = { 92 | "timeout": 16, 93 | "auto_run": True, 94 | "auto_install": True 95 | }, 96 | extensions = { 97 | # type safe kwargs 98 | "Browser": BrowserKwargs(headless=False, engine="google"), 99 | "Email": EmailKwargs(email="amor.budiyanto@gmail.com", password="password") 100 | }, 101 | config = { 102 | "verbose": True, 103 | "conversational": True, 104 | "dev": False 105 | }, 106 | languages = { 107 | # specify custom paths to languages or add custom languages for openmacro 108 | "python": ["C:\Windows\py.EXE", "-c"], 109 | "rust": ["cargo", "script", "-e"] # not supported by default, but can be added! 110 | }, 111 | tts = { 112 | # powered by KoljaB/RealtimeSTT 113 | # options ["SystemEngine", "GTTSEngine", "OpenAIEngine"] 114 | "enabled": True, 115 | "engine": "OpenAIEngine", 116 | "api_key": "sk-example" 117 | } 118 | ) 119 | ``` 120 | And can be extended if you want to build your own app with openmacro: 121 | ```python 122 | ... 123 | 124 | async def main(): 125 | from openmacro.core import Openmacro 126 | 127 | macro = Openmacro(profile) 128 | macro.llm.messages = [] 129 | 130 | async for chunk in macro.chat("Plot an exponential graph for me!", stream=True): 131 | print(chunk, end="") 132 | 133 | import asyncio 134 | asyncio.run(main) 135 | ``` 136 | 137 |
138 | 139 |
140 | JSON 141 | 142 | What your `profile.json` might look like: 143 | ```json 144 | { 145 | "user": { 146 | "name": "Amor", 147 | "version": "1.0.0" 148 | }, 149 | "assistant": { 150 | "name": "Basil", 151 | "personality": "You have a kind, deterministic and professional attitude towards your work and respond in a formal, yet casual manner.", 152 | "messages": [], 153 | "breakers": ["the task is done.", "the conversation is done."] 154 | }, 155 | "safeguards": { 156 | "timeout": 16, 157 | "auto_run": true, 158 | "auto_install": true 159 | }, 160 | "extensions": { 161 | "Browser": { 162 | "headless": false, 163 | "engine": "google" 164 | }, 165 | "Email": { 166 | "email": "amor.budiyanto@gmail.com", 167 | "password": "password" 168 | } 169 | }, 170 | "config": { 171 | "verbose": true, 172 | "conversational": true, 173 | "dev": false 174 | }, 175 | "languages": { 176 | "python": ["C:\\Windows\\py.EXE", "-c"], 177 | "rust": ["cargo", "script", "-e"] 178 | }, 179 | "tts": { 180 | "enabled": true, 181 | "engine": "OpenAIEngine", 182 | "api_key": "sk-example" 183 | 184 | } 185 | } 186 | ``` 187 |
188 | 189 |
190 | TOML 191 | 192 | What your `profile.toml` might look like: 193 | ```toml 194 | [user] 195 | name = "Amor" 196 | version = "1.0.0" 197 | 198 | [assistant] 199 | name = "Basil" 200 | personality = "You have a kind, deterministic and professional attitude towards your work and respond in a formal, yet casual manner." 201 | messages = [] 202 | breakers = ["the task is done.", "the conversation is done."] 203 | 204 | [safeguards] 205 | timeout = 16 206 | auto_run = true 207 | auto_install = true 208 | 209 | [extensions.Browser] 210 | headless = false 211 | engine = "google" 212 | 213 | [extensions.Email] 214 | email = "amor.budiyanto@gmail.com" 215 | password = "password" 216 | 217 | [config] 218 | verbose = true 219 | conversational = true 220 | dev = false 221 | 222 | [languages] 223 | python = ["C:\\Windows\\py.EXE", "-c"] 224 | rust = ["cargo", "script", "-e"] 225 | 226 | [tts] 227 | enabled = true 228 | engine = "SystemEngine" 229 | ``` 230 |
231 | 232 |
233 | YAML 234 | 235 | What your `profile.yaml` might look like: 236 | ```yaml 237 | user: 238 | name: "Amor" 239 | version: "1.0.0" 240 | 241 | assistant: 242 | name: "Basil" 243 | personality: "You have a kind, deterministic and professional attitude towards your work and respond in a formal, yet casual manner." 244 | messages: [] 245 | breakers: 246 | - "the task is done." 247 | - "the conversation is done." 248 | 249 | safeguards: 250 | timeout: 16 251 | auto_run: true 252 | auto_install: true 253 | 254 | extensions: 255 | Browser: 256 | headless: false 257 | engine: "google" 258 | Email: 259 | email: "amor.budiyanto@gmail.com" 260 | password: "password" 261 | 262 | config: 263 | verbose: true 264 | conversational: true 265 | dev: false 266 | 267 | languages: 268 | python: ["C:\\Windows\\py.EXE", "-c"] 269 | rust: ["cargo", "script", "-e"] 270 | 271 | tts: 272 | enabled: true 273 | engine: "SystemEngine" 274 | ``` 275 |
276 | 277 | You can also switch between profiles by running: 278 | ```shell 279 | macro --switch "amor" 280 | ``` 281 | 282 | Profiles also support versions for modularity (uses the latest version by default). 283 | ```shell 284 | macro --switch "amor:1.0.0" 285 | ``` 286 | > [!NOTE] 287 | > All profiles are isolated. LTM from different profiles and versions are not shared. 288 | 289 | You can also quick update a profile. `[BETA]` 290 | ```shell 291 | macro --update "amor" 292 | ``` 293 | Quick updating allows you to easily update and make changes to your profile. Simply make changes to the original profile file, then call above. 294 | 295 | To view all available profiles run: 296 | ```shell 297 | macro --profiles 298 | ``` 299 | 300 | To view all available versions of a profile run: 301 | ```shell 302 | macro --versions 303 | ``` 304 | 305 | ## Extensions 306 | openmacro supports custom RAG extensions for modularity and better capabilities! By default, the `browser` and `email` extensions are installed. 307 | 308 | ### Writing Extensions 309 | Write extensions using the template: 310 | ```python 311 | from typing import TypedDict 312 | class ExtensionKwargs(TypedDict): 313 | ... 314 | 315 | class Extensionname: 316 | def __init__(self): 317 | ... 318 | 319 | @staticmethod 320 | def load_instructions() -> str: 321 | return "" 322 | 323 | ``` 324 | You can find examples [here](https://github.com/Openmacro/openmacro/tree/main/openmacro/extensions). 325 | 326 | > [!TIP] 327 | > classname should not be camelcase, but titlecase instead. 328 | 329 | > [!NOTE] 330 | > creating a type-safe kwargs typeddict is optional but recommended. 331 | 332 | If extesions does not contain a kwarg class, use: 333 | ```python 334 | from openmacro.utils import Kwargs 335 | ``` 336 | 337 | Upload your code to `pypi` for public redistribution using `twine` and `poetry`. 338 | To add it to `openmacro.extensions` for profiles for the AI to use, run: 339 | 340 | ```shell 341 | omi install 342 | ``` 343 | or 344 | ```shell 345 | pip install 346 | omi add 347 | ``` 348 | 349 | You can test your extensions by installing it locally: 350 | ```shell 351 | omi install . 352 | ``` 353 | 354 | ## Todo's 355 | - [x] AI Interpreter 356 | - [X] Web Search Capability 357 | - [X] Async Chunk Streaming 358 | - [X] API Keys Support 359 | - [X] Profiles Support 360 | - [X] Extensions API 361 | - [ ] `WIP` TTS & STT 362 | - [ ] `WIP` Cost Efficient Long Term Memory & Context Manager 363 | - [ ] Semantic File Search 364 | - [ ] Optional Telemetry 365 | - [ ] Desktop, Android & IOS App Interface 366 | 367 | ## Currently Working On 368 | - Optimisations 369 | - Cost efficient long term memory and conversational context managers through vector databases. Most likely powered by [`ChromaDB`](https://github.com/chroma-core/chroma). 370 | - Hooks API and Live Code Output Streaming 371 | 372 | ## Contributions 373 | This is my first major open-source project, so things might go wrong, and there is always room for improvement. You can contribute by raising issues, helping with documentation, adding comments, suggesting features or ideas, etc. Your help is greatly appreciated! 374 | 375 | ## Support 376 | You can support this project by writing custom extensions for openmacro! openmacro aims to be community-powered, as its limitations are based on its capabilities. More extensions mean better chances of completing complex tasks. I will create an official verified list of openmacro extensions sometime in the future! 377 | 378 | ## Contact 379 | You can contact me at [amor.budiyanto@gmail.com](mailto:amor.budiyanto@gmail.com). 380 | -------------------------------------------------------------------------------- /docs/images/demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Openmacro/openmacro/746d3a1932fb60d34b3e543c4f297aef72e9514b/docs/images/demo.png -------------------------------------------------------------------------------- /docs/images/openmacro-title.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/videos/demo.mov: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Openmacro/openmacro/746d3a1932fb60d34b3e543c4f297aef72e9514b/docs/videos/demo.mov -------------------------------------------------------------------------------- /openmacro/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Openmacro/openmacro/746d3a1932fb60d34b3e543c4f297aef72e9514b/openmacro/__init__.py -------------------------------------------------------------------------------- /openmacro/__main__.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | from .core import Openmacro 3 | from .utils import ROOT_DIR, merge_dicts, load_profile, lazy_import, env_safe_replace 4 | import asyncio 5 | import os 6 | 7 | from .cli import main as run_cli 8 | 9 | from pathlib import Path 10 | from rich_argparse import RichHelpFormatter 11 | 12 | from dotenv import load_dotenv 13 | load_dotenv() 14 | 15 | class ArgumentParser(argparse.ArgumentParser): 16 | def __init__(self, 17 | styles: dict, 18 | default: dict, 19 | *args, **kwargs): 20 | 21 | self.default = default 22 | self.profile = default 23 | Path(ROOT_DIR, ".env").touch() 24 | for title, style in styles.items(): 25 | RichHelpFormatter.styles[title] = style 26 | 27 | super().__init__(formatter_class=RichHelpFormatter, 28 | *args, **kwargs) 29 | 30 | self.add_argument("--profiles", action="store_true", help="List all available `profiles`.") 31 | self.add_argument("--versions", metavar='', help="List all available `versions` in a certain `profile`.") 32 | 33 | self.add_argument("--profile", metavar='', type=str, help="Add custom profile to openmacro.") 34 | self.add_argument("--update", metavar='', help="Update changes made in profile.") 35 | self.add_argument("--path", metavar='', help="Add original path to profile for quick updates [BETA].") 36 | self.add_argument("--switch", metavar=':', type=str, help="Switch to a different profile's custom settings.") 37 | 38 | self.add_argument("--default", action="store_true", help="Switch back to default settings.") 39 | 40 | self.add_argument("--api_key", metavar='', type=str, help="Set your API KEY for SambaNova API.") 41 | self.add_argument("--verbose", action="store_true", help="Enable verbose mode for debugging.") 42 | 43 | def parse(self) -> dict: 44 | if os.getenv("PROFILE"): 45 | self.parse_switch(os.getenv("PROFILE")) 46 | 47 | args = vars(self.parse_args()) 48 | for arg, value in args.items(): 49 | if value: 50 | getattr(self, "parse_" + arg)(value) 51 | 52 | return self.profile 53 | 54 | def parse_switch(self, value): 55 | value = value.split(":") 56 | name, version = value[0], value[-1] 57 | 58 | path = Path(ROOT_DIR, "profiles", name) 59 | if not path.is_dir(): 60 | raise FileNotFoundError(f"Profile `{value}` does not exist") 61 | 62 | if not len(value) == 2: 63 | versions = [versions.name for versions in Path(ROOT_DIR, "profiles", name).iterdir()] 64 | version = sorted(versions)[-1] 65 | 66 | env_safe_replace(Path(ROOT_DIR, ".env"), 67 | {"PROFILE":f"{name}:{version}"}) 68 | 69 | self.profile = merge_dicts(self.profile, load_profile(Path(path, version, "profile.json"))) 70 | 71 | def parse_path(self, value): 72 | name, version = os.getenv("PROFILE", "").split(":") or ("User", "1.0.0") 73 | env = Path(ROOT_DIR, "profiles", name, ".env") 74 | env.parent.mkdir(exist_ok=True) 75 | env.touch() 76 | env_safe_replace(env, {"ORIGINAL_PROFILE_PATH": value}) 77 | 78 | def parse_api_key(self, value): 79 | self.profile["env"]["api_key"] = value 80 | 81 | def parse_verbose(self, value): 82 | self.profile["config"]["verbose"] = True 83 | 84 | def parse_default(self, value): 85 | self.profile = self.default 86 | 87 | def parse_profiles(self, value): 88 | profiles = set(profiles.name 89 | for profiles in Path(ROOT_DIR, "profiles").iterdir()) 90 | print(f"Profiles Available: {profiles}") 91 | 92 | def parse_versions(self, value): 93 | profiles = set(profiles.name 94 | for profiles in Path(ROOT_DIR, "profiles", value).iterdir() 95 | if profiles.is_dir()) 96 | print(f"Versions Available: {profiles}") 97 | 98 | def parse_update(self, name): 99 | toml = lazy_import("toml") 100 | env = Path(ROOT_DIR, "profiles", name, ".env") 101 | 102 | if not env.is_file(): 103 | raise FileNotFoundError("`.env` missing from profile. Add `.env` by calling `macro --path `.") 104 | 105 | with open(env, "r") as f: 106 | args_path = Path(toml.load(f)["ORIGINAL_PROFILE_PATH"]) 107 | 108 | if not args_path.is_file(): 109 | raise FileNotFoundError(f"Original profile path `{args_path}` has been moved. Update original profile path by calling `macro --path `.") 110 | 111 | profile = load_profile(args_path) 112 | versions = [versions.name 113 | for versions in Path(ROOT_DIR, "profiles", name).iterdir()] 114 | latest = sorted(versions)[-1] 115 | 116 | if latest > (version := profile["user"].get("version", "1.0.0")): 117 | version = latest 118 | 119 | major, minor, patch = map(int, version.split(".")) 120 | profile["user"]["version"] = f"{major}.{minor}.{patch+1}" 121 | 122 | self.profile = merge_dicts(self.profile, profile) 123 | 124 | def parse_profile(self, path): 125 | self.profile = merge_dicts(self.profile, load_profile(path)) 126 | self.profile["env"]["path"] = path 127 | 128 | def main(): 129 | Path(ROOT_DIR, "profiles").mkdir(exist_ok=True) 130 | from .profile.template import profile 131 | 132 | parser = ArgumentParser( 133 | styles={ 134 | "argparse.groups":"bold", 135 | "argparse.args": "#79c0ff", 136 | "argparse.metavar": "#2a6284" 137 | }, 138 | default=profile, 139 | description="[#92c7f5]O[/#92c7f5][#8db9fe]pe[/#8db9fe][#9ca4eb]nm[/#9ca4eb][#bbb2ff]a[/#bbb2ff][#d3aee5]cr[/#d3aee5][#caadea]o[/#caadea] is a multimodal assistant, code interpreter, and human interface for computers. [dim](0.2.8)[/dim]", 140 | ) 141 | 142 | profile = parser.parse() 143 | macro = Openmacro(profile) 144 | asyncio.run(run_cli(macro)) 145 | 146 | if __name__ == "__main__": 147 | main() 148 | -------------------------------------------------------------------------------- /openmacro/cli.py: -------------------------------------------------------------------------------- 1 | from rich import print 2 | from rich.markdown import Markdown 3 | from datetime import datetime 4 | from .speech import Speech 5 | 6 | import threading 7 | 8 | 9 | def to_chat(lmc: dict, content = True) -> str: 10 | _type, _role, _content, _format = (lmc.get("type", "message"), 11 | lmc.get("role", "assistant"), 12 | lmc.get("content", "None"), 13 | lmc.get("format", None)) 14 | 15 | time = datetime.now().strftime("%I:%M %p %d/%m/%Y") 16 | display = f"[bold #4a4e54]\u25CB ({time})[/bold #4a4e54] [italic bold]{_role}[/italic bold]" 17 | if content: 18 | return (display, _content) 19 | return display 20 | 21 | async def main(macro): 22 | split = False 23 | hidden = False 24 | 25 | if macro.profile["tts"]["enabled"]: 26 | speech = Speech(tts=macro.profile["tts"]) 27 | 28 | while True: 29 | user = to_chat({"role": macro.profile["user"]["name"]}, content=False) 30 | print(user) 31 | try: 32 | query = input('~ ') or "plot an exponential graph" 33 | except Exception as e: 34 | print("Exiting `openmacro`...") 35 | exit() 36 | 37 | assistant = to_chat({"role": macro.name}, content=False) 38 | print("\n" + assistant) 39 | async for chunk in macro.chat(query, stream=True): 40 | if isinstance(chunk, dict): 41 | print("\n") 42 | computer, content = to_chat(chunk) 43 | print(computer) 44 | print(content) 45 | 46 | if chunk.get("role", "").lower() == "computer": 47 | assistant = to_chat({"role": macro.name}, content=False) 48 | print("\n" + assistant) 49 | 50 | 51 | elif macro.profile["tts"]["enabled"]: 52 | if "" in chunk: 53 | hidden = True 54 | elif "" in chunk: 55 | hidden = False 56 | 57 | if not hidden and not "" in chunk: 58 | speech.tts.stream(chunk) 59 | 60 | if isinstance(chunk, str) and not (chunk == ""): 61 | print(chunk, end="") 62 | 63 | print("\n") -------------------------------------------------------------------------------- /openmacro/computer/__init__.py: -------------------------------------------------------------------------------- 1 | import shutil 2 | import subprocess 3 | from typing import List, Dict 4 | from functools import partial 5 | from pathlib import Path 6 | from ..utils import ROOT_DIR 7 | import openmacro.extensions as extensions 8 | 9 | class Computer: 10 | def __init__(self, 11 | profile_path: Path | str = None, 12 | paths: Dict[str, list] = None, 13 | extensions: Dict[str, object] = None): 14 | 15 | self.profile_path = profile_path or Path(ROOT_DIR, "profile", "template.py") 16 | self.extensions = extensions or {} 17 | self.custom_paths = paths or {} 18 | self.supported = self.available() 19 | 20 | def inject_kwargs(self, code): 21 | for extension, vals in self.extensions.items(): 22 | if extension in code: 23 | kwarg_str = ', '.join(f'{kwarg}={val!r}' for kwarg, val in vals.items()) 24 | code = code.replace(f"{extension}()", f"{extension}({kwarg_str})") 25 | return code 26 | 27 | def load_instructions(self): 28 | return "\n\n".join( 29 | getattr(extensions, extension).load_instructions() 30 | for extension in self.extensions.keys() 31 | ) 32 | 33 | def available(self) -> Dict[str, str]: 34 | languages = { 35 | "python": ["py", "python", "python3"], 36 | "js": ["bun", "deno", "node"], 37 | "r": ["R", "rscript"], 38 | "java": ["java"], 39 | "cmd": ["cmd"], 40 | "powershell": ["powershell"], 41 | "applescript": ["osascript"], 42 | "bash": ["bash"], 43 | } 44 | 45 | args = { 46 | "python": "-c", 47 | "cmd": "/c", 48 | "powershell": "-Command", 49 | "applescript": "-e", 50 | "bash": "-c", 51 | "js": "-e", 52 | "r": "-e", 53 | "java": "-e" 54 | } 55 | 56 | supported = {} 57 | for lang, command in languages.items(): 58 | if (path := self.check(command)): 59 | supported[lang] = [path, args[lang]] 60 | supported |= self.custom_paths 61 | 62 | return supported 63 | 64 | def check(self, exes) -> bool: 65 | for exe in exes: 66 | if (exe := shutil.which(exe)): 67 | return exe 68 | 69 | def run(self, code: str, language: str ='python') -> str: 70 | try: 71 | command = self.supported.get(language, None) 72 | if command is None: 73 | return f"Openmacro does not support the language: {language}" 74 | 75 | if language == "python": 76 | code = self.inject_kwargs(code) 77 | 78 | result = subprocess.run(command + [code], capture_output=True, text=True) 79 | if result.stdout or result.stderr: 80 | return (result.stdout + "\n" + result.stderr).strip() 81 | if result.returncode == 0: 82 | return (f"The following code did not generate any console text output, but may generate other output.") 83 | return (f"Command executed with exit code: {result.returncode}") 84 | 85 | except Exception as e: 86 | return f"An error occurred: {e}" 87 | -------------------------------------------------------------------------------- /openmacro/core/__init__.py: -------------------------------------------------------------------------------- 1 | from ..computer import Computer 2 | from ..profile import Profile 3 | from ..profile.template import profile as default_profile 4 | 5 | from ..llm import LLM, to_lmc, to_chat, interpret_input 6 | from ..utils import ROOT_DIR, OS, generate_id, get_relevant, load_profile, load_prompts, init_profile 7 | 8 | from ..memory.server import Manager 9 | from ..memory.client import Memory 10 | from chromadb.config import Settings 11 | 12 | from datetime import datetime 13 | from pathlib import Path 14 | import threading 15 | import asyncio 16 | import json 17 | 18 | from dotenv import load_dotenv 19 | 20 | class Openmacro: 21 | """ 22 | The core of all operations occurs here. 23 | Where the system breaks down requests from the user and executes them. 24 | """ 25 | def __init__( 26 | self, 27 | profile: Profile = None, 28 | profile_path: Path | str = None, 29 | messages: list | None = None, 30 | prompts_dir: Path | None = None, 31 | memories_dir: Path | None = None, 32 | tts: bool = False, 33 | stt: bool = False, 34 | verbose: bool = False, 35 | conversational: bool = False, 36 | telemetry: bool = False, 37 | local: bool = False, 38 | computer = None, 39 | dev = False, 40 | llm = None, 41 | extensions: dict = {}, 42 | breakers = ("the task is done.", "the conversation is done.")) -> None: 43 | 44 | profile = profile or load_profile(profile_path) or default_profile 45 | self.profile = profile 46 | 47 | # setup paths 48 | paths = self.profile["paths"] 49 | self.prompts_dir = prompts_dir or paths.get("prompts") 50 | self.memories_dir = memories_dir or paths.get("memories") or Path(ROOT_DIR, "profiles", profile["user"]["name"], profile["user"]["version"]) 51 | 52 | load_dotenv() 53 | load_dotenv(dotenv_path=Path(self.memories_dir.parent, ".env")) 54 | 55 | init_profile(self.profile, 56 | self.memories_dir) 57 | 58 | # setup other instances 59 | self.computer = computer or Computer(profile_path=profile.get("path", None), 60 | paths=profile.get("languages", {}), 61 | extensions=extensions or profile.get("extensions", {})) 62 | 63 | # logging + debugging 64 | self.verbose = verbose or profile["config"]["verbose"] 65 | self.conversational = conversational or profile["config"]["conversational"] 66 | self.tts = tts or profile["config"].get("tts", {}) 67 | self.stt = tts or profile["config"].get("stt", {}) 68 | 69 | # loop breakers 70 | self.breakers = breakers or profile["assistant"]["breakers"] 71 | 72 | # settings 73 | self.safeguards = profile["safeguards"] 74 | self.dev = dev or profile["config"]["dev"] 75 | 76 | # setup setup variables 77 | self.info = { 78 | "assistant": profile['assistant']['name'], 79 | "personality": profile['assistant']['personality'], 80 | "username": profile["user"]["name"], 81 | "version": profile["user"]["version"], 82 | "os": OS, 83 | "supported": list(self.computer.supported), 84 | "extensions": extensions or self.computer.load_instructions() 85 | } 86 | 87 | # setup memory 88 | self.memory_manager = Manager(path=self.memories_dir, telemetry=telemetry) 89 | self.memory_manager.serve_and_wait() 90 | 91 | self.memory = Memory(host='localhost', 92 | port=8000, 93 | settings=Settings(anonymized_telemetry=telemetry)) 94 | self.ltm = self.memory.get_or_create_collection("ltm") 95 | 96 | # restart stm cache 97 | # self.memory.delete_collection(name="cache") 98 | self.cache = self.memory.get_or_create_collection("cache") 99 | 100 | # experimental (not yet implemented) 101 | self.local = local or profile["config"]["local"] 102 | 103 | # setup prompts 104 | self.prompts = load_prompts(self.prompts_dir, 105 | self.info, 106 | self.conversational) 107 | 108 | # setup llm 109 | self.name = profile['assistant']["name"] 110 | self.llm = llm or LLM(messages=messages, 111 | verbose=verbose, 112 | system=self.prompts['initial']) 113 | 114 | self.loop = asyncio.get_event_loop() 115 | 116 | async def remember(self, message): 117 | document = self.ltm.query(query_texts=[message], 118 | n_results=3, 119 | include=["documents", "metadatas", "distances"]) 120 | 121 | # filter by distance 122 | snapshot = get_relevant(document, threshold=1.45) 123 | #print(snapshot) 124 | if not snapshot.get("documents"): 125 | return [] 126 | 127 | memories = [] 128 | for document, metadata in zip(snapshot.get("documents", []), 129 | snapshot.get("metadatas", [])): 130 | text = f"{document}\n[metadata: {metadata}]" 131 | memories.append(to_lmc(text, role="Memory", type="memory snapshot")) 132 | 133 | #print(memories) 134 | return memories 135 | 136 | def add_memory(self, memory): 137 | # check if ai fails to format json 138 | try: memory = json.loads(memory) 139 | except: return 140 | 141 | # check memory defined correctly 142 | if not memory.get("memory"): 143 | return 144 | 145 | kwargs = {"documents": [memory["memory"]], 146 | "ids": [generate_id()], 147 | "metadatas": [memory.get("metadata", {}) | 148 | {"time": datetime.now().strftime("%Y-%m-%d %H:%M:%S")}]} 149 | self.ltm.add(**kwargs) 150 | 151 | 152 | def memorise(self, messages): 153 | memory = self.llm.chat("\n\n".join(map(to_chat, messages)), 154 | system=self.prompts["memorise"], 155 | remember=False, 156 | stream=False) 157 | if not memory: return 158 | self.add_memory(memory) 159 | 160 | def thread_memorise(self, messages: list[str]): 161 | thread = threading.Thread(target=self.memorise, args=[messages]) 162 | thread.start() 163 | thread.join() 164 | 165 | async def streaming_chat(self, 166 | message: str = None, 167 | remember=True, 168 | timeout=None, 169 | lmc=False): 170 | 171 | timeout = timeout or self.safeguards["timeout"] 172 | 173 | response, notebooks, hidden = "", {}, False 174 | for _ in range(timeout): 175 | # TODO: clean up. this is really messy. 176 | 177 | # remember anything relevant 178 | 179 | if not lmc and (memory := await self.remember(message)): 180 | #print(memory) 181 | # if self.dev or self.verbose: 182 | # for chunk in memory: 183 | # yield chunk 184 | self.llm.messages += memory 185 | 186 | async for chunk in self.llm.chat(message=message, 187 | stream=True, 188 | remember=remember, 189 | lmc=lmc): 190 | response += chunk 191 | if self.conversational: 192 | if self.verbose or self.dev: 193 | pass 194 | elif "" in chunk: 195 | hidden = True 196 | continue 197 | elif "" in chunk: 198 | hidden = False 199 | continue 200 | 201 | if not hidden: 202 | yield chunk 203 | 204 | if self.conversational: 205 | yield "" 206 | 207 | # memorise if relevant 208 | memorise = [] if lmc else [to_lmc(message, role="User")] 209 | self.thread_memorise(self.llm.messages[:-3] + memorise + [to_lmc(response)]) 210 | 211 | # because of this, it's only partially async 212 | # will fix in future versions 213 | lmc = False 214 | 215 | for chunk in interpret_input(response): 216 | if chunk.get("type", None) == "code": 217 | language, code = chunk.get("format"), chunk.get("content") 218 | if language in notebooks: notebooks[language] += "\n\n" + code 219 | else: notebooks[language] = code 220 | 221 | elif "let's run the code" in chunk.get("content").lower(): 222 | for language, code in notebooks.items(): 223 | output = self.computer.run(code, language) 224 | message, lmc = to_lmc(output, role="computer", format="output"), True 225 | if self.dev or self.verbose: 226 | yield message 227 | notebooks = {} 228 | 229 | response = "" 230 | if not lmc or chunk.get("content", "").lower().endswith(self.breakers): 231 | return 232 | 233 | raise Warning("Openmacro has exceeded it's timeout stream of thoughts!") 234 | 235 | async def _gather(self, gen): 236 | return "".join([str(chunk) async for chunk in gen]) 237 | 238 | def chat(self, 239 | message: str | None = None, 240 | stream: bool = False, 241 | remember: bool = True, 242 | lmc: bool = False, 243 | timeout=16): 244 | timeout = timeout or self.safeguards["timeout"] 245 | 246 | gen = self.streaming_chat(message, remember, timeout, lmc) 247 | if stream: return gen 248 | return self.loop.run_until_complete(self._gather(gen)) -------------------------------------------------------------------------------- /openmacro/core/prompts/conversational.txt: -------------------------------------------------------------------------------- 1 | 2 | YOU ARE CONVERSATIONAL MEANING PLANNING/ THOUGHTS SHOULD NOT BE SHOWN TO THE USER. 3 | All responses within tags will not be shown. 4 | Here is an example of an ideal response: 5 | 6 | Sure thing! Let me find the weather for you! 7 | 8 | # Checking the Weather 9 | To find out the current weather, we can use the Browser extension to search for the latest weather updates. 10 | 11 | ```python 12 | from openmacro.extensions import Browser 13 | browser = Browser() 14 | weather = browser.widget_search("weather today", widget="weather") 15 | print(weather) 16 | ``` 17 | Let's run the code! 18 | 19 | 20 | IMPORTANT TIP: respond as humanly as possible. Do not state Let's run the code outside of a hidden tag since the user will not see it. -------------------------------------------------------------------------------- /openmacro/core/prompts/initial.txt: -------------------------------------------------------------------------------- 1 | You are {assistant}, a world-class assistant working under Openmacro that can complete any goal by executing code. 2 | 3 | User's Name: {username} 4 | User's OS: {os} 5 | 6 | -------------------------------------------------------------------------------- /openmacro/core/prompts/instructions.txt: -------------------------------------------------------------------------------- 1 | For advanced requests, start by writing a plan. 2 | When you execute code, it will be executed **on the user's machine**. The user has given you **full and complete permission** to execute any code necessary to complete the task. Execute the code. 3 | Run **any code** to achieve the goal, and if at first you don't succeed, try again and again. 4 | When a user refers to a filename, they're likely referring to an existing file in the directory you're currently executing code in. 5 | Write messages to the user in Markdown. 6 | In general, try to **make plans** with as few steps as possible. As for actually executing code to carry out that plan, for *stateful* languages (like python, javascript, shell, but NOT for html) **it's critical not to try to do everything in one code block.** You should try something, print information about it, then continue from there in tiny, informed steps. You will never get it on the first try, and attempting it in one go will often lead to errors you can't see. **You can only run:** {supported} on the user's computer. 7 | You are capable of **any** task. 8 | 9 | To run code on the user's machine, format them in a markdown code block like so: 10 | 11 | --- EXAMPLE --- 12 | # Printing Hello, User! 13 | First, we get input. 14 | ```python 15 | user = input() 16 | ``` 17 | Then we print the output! 18 | ```python 19 | print(f'Hello', user) 20 | ``` 21 | --- END EXAMPLE --- 22 | 23 | This will act like an Interactive Python Jupyter Notebook file but for all languages, only code in the markdown codeblock is ran. 24 | To run the code on the user's computer state exactly "`Let's run the code.`" somewhere within your reply. 25 | Output of the code will be returned to you. 26 | **NOTE, EVERY TIME CODE IS RAN, THE SCRIPT/ NOTEBOOK IS CLEARED.** 27 | 28 | # THE EXTENSIONS RAG API 29 | There are many python RAG extensions you can import to complete many tasks. 30 | 31 | Import apps like so: 32 | ```python 33 | from openmacro.extensions import Browser 34 | browser = Browser() 35 | results = browser.search("latest msft stock prices") 36 | print(results) 37 | ``` 38 | 39 | RAG extensions you can access include: 40 | ```python 41 | {extensions} 42 | ``` 43 | 44 | You can install pip packages like so: 45 | ```python 46 | from openmacro.utils import lazy_import 47 | lazy_import("pygame", install=True, void=True) 48 | ``` 49 | If you want to install and instantly use the package, remove the void param: 50 | ```python 51 | np = lazy_import("numpy", install=True) 52 | print(np.linspace(1, 10)) 53 | ``` 54 | 55 | Always wait for the code to be executed first before ending the task to ensure output is as expected. 56 | {personality} -------------------------------------------------------------------------------- /openmacro/core/prompts/memorise.txt: -------------------------------------------------------------------------------- 1 | You are a world-class long-term memory management AI RAG Tool. Given a concise but detailed conversation context between a user and a conversational chatbot, summarise key points RELEVANT FOR FUTURE REFERENCE. DO NOT MEMORISE USELESS SHORT TERM POINTS. 2 | 3 | EXAMPLES OF RELEVANT FUTURE REFERENCES AND SUMMARIES: 4 | 1. User loves winter season 5 | 2. User has friends called Tyler, Callum, and Sam 6 | 3. User has an internship interview for PwC tomorrow (12 October 2024) 7 | 4. User lives in Christchurch, NZ 8 | 5. User is studying Bachelor's of Software Engineering at University of Canterbury 9 | 6. User has a goal to study overseas 10 | 7. User loves the subject maths 11 | 12 | **IMPORTANT: RETURN ONLY IN THE FOLLOWING JSON FORMAT** 13 | **IMPORTANT: FIELDS ARE ONLY ALLOWED TO BE str, int, float or bool** 14 | **IMPORTANT: DO NOT REPEAT THE SAME MEMORY IF PREVIOUSLY ALREADY MENTIONED** 15 | { 16 | "memory": "User wants to go to restaurant called 'Big Chicken' with John", 17 | "metadata": { 18 | "involved": "User, John" 19 | } 20 | } 21 | 22 | **IMPORTANT: IF THERE IS NOTHING RELEVANT, SIMPLY RETURN:** 23 | {} -------------------------------------------------------------------------------- /openmacro/extensions/__init__.py: -------------------------------------------------------------------------------- 1 | from .browser import Browser, BrowserKwargs 2 | from .email import Email, EmailKwargs 3 | 4 | from ..utils import ROOT_DIR, Kwargs 5 | from pathlib import Path 6 | import importlib 7 | 8 | def load_extensions(): 9 | with open(Path(ROOT_DIR, "extensions", "extensions.txt"), "r") as f: 10 | extensions = f.read().splitlines() 11 | 12 | for module_name in extensions: 13 | module = importlib.import_module(module_name) 14 | globals()[module_name] = getattr(module, module_name.title()) 15 | if (kwargs := getattr(module, module_name.title()+"Kwargs")): 16 | globals()[module_name+"Kwargs"] = kwargs 17 | 18 | load_extensions() -------------------------------------------------------------------------------- /openmacro/extensions/browser/__init__.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | from pathlib import Path 4 | from ...llm import LLM 5 | from ...utils import ROOT_DIR, lazy_import 6 | from ...memory.client import Memory 7 | from chromadb.config import Settings 8 | 9 | # playwright = lazy_import("playwright", 10 | # scripts=[["playwright", "install"]]) 11 | 12 | from playwright.async_api import async_playwright 13 | 14 | from .utils.general import to_markdown 15 | from ...utils import get_relevant, generate_id 16 | import importlib 17 | import browsers 18 | import random 19 | import json 20 | import toml 21 | 22 | from typing import TypedDict 23 | 24 | class BrowserKwargs(TypedDict): 25 | headless: bool 26 | engine: str 27 | 28 | class Browser: 29 | def __init__(self, 30 | headless=True, 31 | engine="google"): 32 | # Temp solution, loads widgets from ALL engines 33 | # Should only load widgets from chosen engine 34 | 35 | # points to current openmacro instance 36 | self.headless = headless 37 | self.llm = LLM() 38 | self.context = Memory(host='localhost', 39 | port=8000, 40 | settings=Settings(anonymized_telemetry=False)) 41 | self.browser_context = self.context.get_or_create_collection("cache") 42 | 43 | with open(Path(__file__).parent / "src" / "engines.json", "r") as f: 44 | self.engines = json.load(f) 45 | self.browser_engine = 'google' 46 | 47 | path = ".utils." 48 | for engine, data in self.engines.items(): 49 | module = importlib.import_module(path + engine, package=__package__) 50 | self.engines[engine]["widgets"] = {widget: getattr(module, lib) for widget, lib in data["widgets"].items()} 51 | 52 | 53 | default_path = Path(__file__).parent / "config.default.toml" 54 | if (config_path := Path(__file__).parent / "config.toml").is_file(): 55 | default_path = config_path 56 | 57 | with open(default_path, "r") as f: 58 | self.settings = toml.load(f) 59 | 60 | for key, value in self.settings['search'].items(): 61 | self.settings['search'][key] = frozenset(value) 62 | 63 | # Init browser at runtime for faster speeds in the future 64 | self.loop = asyncio.get_event_loop() 65 | self.loop.run_until_complete(self.init_playwright()) 66 | 67 | @staticmethod 68 | def load_instructions(): 69 | with open(Path(ROOT_DIR, "extensions", "browser", "docs", "instructions.md"), "r") as f: 70 | return f.read() 71 | 72 | async def close_playwright(self): 73 | await self.browser.close() 74 | await self.playwright.stop() 75 | 76 | async def init_playwright(self): 77 | installed_browsers = {browser['display_name']:browser 78 | for browser in browsers.browsers()} 79 | 80 | supported = ("Google Chrome", "Mozilla Firefox", "Microsoft Edge") 81 | for browser in supported: 82 | if (selected := installed_browsers.get(browser, {})): 83 | break 84 | 85 | self.playwright = await async_playwright().start() 86 | with open(Path(Path(__file__).parent, "src", "user_agents.txt"), "r") as f: 87 | self.user_agent = random.choice(f.read().split('\n')) 88 | 89 | path, browser_type = selected.get("path"), selected.get("browser_type", "unknown") 90 | self.browser_type = browser_type 91 | if (browser_type == "firefox" 92 | or browser_type == "unknown" 93 | or not selected): 94 | 95 | await self.init_gecko() 96 | else: 97 | try: 98 | await self.init_chromium(browser, path, browser_type) 99 | except: 100 | await self.init_gecko() 101 | 102 | if not self.browser: 103 | raise Exception("Browser initialization failed.") 104 | 105 | async def init_chromium(self, browser, local_browser, browser_type): 106 | # supports user profiles (for saved logins) 107 | # temp solution, use default profile 108 | 109 | profile_path = Path(Path.home(), "AppData", "Local", *browser.split(), "User Data", "Default") 110 | self.browser = await self.playwright.chromium.launch_persistent_context(executable_path=local_browser, 111 | channel=browser_type, 112 | headless=self.headless, 113 | user_agent=self.user_agent, 114 | user_data_dir=profile_path) 115 | 116 | # await self.browser.route("**/*", self.handle_route) 117 | 118 | 119 | async def init_gecko(self): 120 | # doesn't support user profiles 121 | # because of playwright bug with gecko based browsers 122 | 123 | self.browser = await self.playwright.firefox.launch_persistent_context(headless=self.headless, 124 | user_agent=self.user_agent) 125 | 126 | # await self.browser.route("**/*", self.handle_route) 127 | 128 | async def check_visibility_while_waiting(self, page, check_selector, wait_selector, timeout=60000): 129 | start_time = asyncio.get_event_loop().time() 130 | end_time = start_time + timeout / 1000 131 | 132 | while asyncio.get_event_loop().time() < end_time: 133 | if await page.is_visible(check_selector): 134 | return True 135 | 136 | try: 137 | return await page.wait_for_selector(wait_selector, state='visible', timeout=1000) 138 | except: 139 | pass # wait_selector did not appear within the timeout 140 | 141 | return False # Timeout reached, return False 142 | 143 | def handle_route(self, route, request): 144 | ignore = list(self.settings["search"]["ignore_resources"]) 145 | if any(request.url.endswith(ext) for ext in ignore): 146 | route.abort() 147 | else: 148 | route.continue_() 149 | 150 | def perplexity_search(self, query: str): 151 | return self.loop.run_until_complete(self.run_perplexity_search(query)) 152 | 153 | async def run_perplexity_search(self, query: str): 154 | async with await self.browser.new_page() as page: 155 | try: 156 | await page.goto("https://www.perplexity.ai/search/new?q=" + query) 157 | copy = await self.check_visibility_while_waiting(page, 158 | '.zone-name-title', # cloudflare 159 | '.flex.items-center.gap-x-xs > button:first-child') # perplexity 160 | 161 | # cloudflare auth is blocking perplexity :( 162 | if isinstance(copy, bool): 163 | return "" 164 | 165 | await copy.click() 166 | text = await page.evaluate('navigator.clipboard.readText()') 167 | except Exception as e: # will add proper error handling 168 | return "" 169 | return text 170 | 171 | 172 | async def playwright_search(self, 173 | query: str, 174 | n: int = 3, 175 | engine: str = "google"): 176 | 177 | 178 | results = (f"Error: An error occured with {engine} search.",) 179 | async with await self.browser.new_page() as page: 180 | 181 | engine = self.engines.get(self.browser_engine, engine) 182 | await page.goto(engine["engine"] + query) 183 | 184 | # wacky ahh searching here 185 | results = () 186 | keys = {key: None for key in engine["search"].keys()} 187 | results += tuple(keys.copy() for _ in range(n)) 188 | 189 | for key, selector in engine["search"].items(): 190 | elements = (await page.query_selector_all(selector))[:n] 191 | for index, elem in enumerate(elements): 192 | results[index][key] = (await elem.get_attribute('href') 193 | if key == "link" 194 | else await elem.inner_text()) 195 | 196 | return results 197 | 198 | async def playwright_load(self, url, clean: bool = False, to_context=False, void=False): 199 | async with await self.browser.new_page() as page: 200 | await page.goto(url) 201 | 202 | if not clean: 203 | return await page.content() 204 | 205 | body = await page.query_selector('body') 206 | html = await body.inner_html() 207 | 208 | contents = to_markdown(html, 209 | ignore=['header', 'footer', 'nav', 'navbar'], 210 | ignore_classes=['footer']).strip() 211 | 212 | # CONCEPT 213 | # add to short-term openmacro vectordb 214 | # acts like a cache and local search engine 215 | # for previous web searches 216 | # uses embeddings to view relevant searches 217 | 218 | # will actually use a temp collection 219 | # stm should act like a cache 220 | 221 | if to_context: 222 | # temp, will improve 223 | contents = contents.split("###") 224 | self.browser_context.add( 225 | documents=contents, 226 | metadatas=[{"source": "browser"} 227 | for _ in range(len(contents))], # filter on these! 228 | ids=[generate_id() 229 | for _ in range(len(contents))], # unique for each doc 230 | ) 231 | 232 | if not void: 233 | return contents 234 | 235 | 236 | def search(self, 237 | query: str, 238 | n: int = 3, 239 | cite: bool = False, 240 | engine: str = "google", 241 | local: bool = False): 242 | 243 | # TODO: add a cache 244 | 245 | # search WITH perplexity.ai 246 | if not local and (result := self.perplexity_search(query)): 247 | return result 248 | 249 | # FALLBACK 250 | # search LIKE perplexity.ai (locally) 251 | # uses embeddings :D 252 | 253 | sites = self.loop.run_until_complete(self.playwright_search(query, n, engine)) 254 | self.parallel(*(self.playwright_load(url=site["link"], 255 | clean=True, 256 | to_context=True, 257 | void=True) 258 | for site in sites)) 259 | 260 | n = n*3 if 10 > n*3 else 9 261 | relevant = get_relevant(self.browser_context.query(query_texts=[query], 262 | n_results=n), 263 | clean=True) 264 | 265 | prompt = self.settings["prompts"]["summarise"] 266 | if cite: 267 | prompt += self.settings["prompts"]["citations"] 268 | 269 | result = self.llm.chat(relevant, 270 | role="browser", 271 | system=prompt) 272 | return result 273 | 274 | 275 | def widget_search(self, 276 | query: str, 277 | widget: str, 278 | engine: str = "google") -> dict: 279 | 280 | results = self.loop.run_until_complete(self.run_widget_search(query, widget, engine)) 281 | 282 | # fallback to perplexityy 283 | if not results or results.get("error"): 284 | results |= {"results": self.loop.run_until_complete(self.run_perplexity_search(query))} 285 | 286 | if not results["results"]: 287 | return {"error": "It seems like your query does not show any widgets."} 288 | 289 | return results 290 | 291 | async def run_widget_search(self, 292 | query: str, 293 | widget: str, 294 | engine: str = "google") -> dict: 295 | 296 | engine = self.engines.get(self.browser_engine, {}) 297 | async with await self.browser.new_page() as page: 298 | await page.goto(engine["engine"] + query) 299 | 300 | try: 301 | if (function := engine.get("widgets", {}).get(widget, None)): 302 | results = {"results": (await function(self, page))} or {} 303 | except Exception as e: 304 | results = {"error": f"An error occurred: {str(e)}, results are fallback from perplexity.ai."} 305 | return results 306 | 307 | 308 | def parallel(self, *funcs, void=False): 309 | results = self.loop.run_until_complete(self.run_parallel(*funcs)) 310 | if not void: 311 | return results 312 | 313 | async def run_parallel(self, *funcs): 314 | return tuple(await asyncio.gather(*funcs)) 315 | -------------------------------------------------------------------------------- /openmacro/extensions/browser/config.default.toml: -------------------------------------------------------------------------------- 1 | [prompts] 2 | summarise="You are a web search specialist and your duty is to summarise researched information from the browser into 2 to 3 paragraphs in markdown. You may use bullet points and other markdown formatting to convey the summary. Summarise the following." 3 | citations="" 4 | 5 | [search] 6 | ignore_resources=["png", "jpg", "jpeg", "svg", "gif", "css", "woff", "woff2", "mp3", "mp4"] 7 | ignore_words=["main menu", "move to sidebar", "navigation", "contribute", "search", "appearance", "tools", "personal tools", "pages for logged out editors", "move to sidebar", "hide", "show", "toggle the table of contents", "general", "actions", "in other projects", "print/export"] -------------------------------------------------------------------------------- /openmacro/extensions/browser/docs/instructions.md: -------------------------------------------------------------------------------- 1 | # BROWSER APP 2 | browser = Browser() 3 | results = browser.search(query, n=1) # Returns array of summaries based on results 4 | showtimes = browser.widget_search("weather today", widget="weather") # Returns dictionary of Google Snippet for showtimes of query. Can be used with other Google Snippet options including ['showtimes', 'weather', 'events', 'reviews'] 5 | print(results) # Print out returned values -------------------------------------------------------------------------------- /openmacro/extensions/browser/src/engines.json: -------------------------------------------------------------------------------- 1 | { 2 | "google": { 3 | "engine": "https://www.google.com/search?q=", 4 | "search": { 5 | "title": "h3.LC20lb", 6 | "description": "div.r025kc", 7 | "link": "div.yuRUbf > div > span > a" 8 | }, 9 | "widgets": { 10 | "weather": "get_weather", 11 | "showtimes": "get_showtimes", 12 | "events": "get_events", 13 | "reviews": "get_reviews" 14 | } 15 | } 16 | } -------------------------------------------------------------------------------- /openmacro/extensions/browser/src/user_agents.txt: -------------------------------------------------------------------------------- 1 | Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36 2 | Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36 3 | Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36 4 | Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36 5 | Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36 6 | Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36 7 | Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36 8 | Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:129.0) Gecko/20100101 Firefox/129.0 -------------------------------------------------------------------------------- /openmacro/extensions/browser/tests/tests.py: -------------------------------------------------------------------------------- 1 | from .. import Browser 2 | 3 | browser = Browser(headless=False) 4 | print(browser.search("Inside Out 2")) -------------------------------------------------------------------------------- /openmacro/extensions/browser/utils/general.py: -------------------------------------------------------------------------------- 1 | from markdownify import markdownify as md 2 | from bs4 import BeautifulSoup 3 | import numpy as np 4 | import random 5 | import string 6 | import re 7 | 8 | # might publish as a new module 9 | def filter_markdown(markdown): 10 | filtered_lines = [] 11 | consecutive_new_lines = 0 12 | rendered = re.compile(r'.*\]\(http.*\)') 13 | embed_line = re.compile(r'.*\]\(.*\)') 14 | 15 | for line in markdown.split('\n'): 16 | line: str = line.strip() 17 | 18 | if embed_line.match(line) and not rendered.match(line): 19 | continue 20 | 21 | if '[' in line and ']' not in line: 22 | line = line.replace('[', '') 23 | elif ']' in line and '[' not in line: 24 | line = line.replace(']', '') 25 | 26 | if len(line) > 2: 27 | filtered_lines.append(line) 28 | consecutive_new_lines = 0 29 | elif line == '' and consecutive_new_lines < 1: 30 | filtered_lines.append('') 31 | consecutive_new_lines += 1 32 | 33 | return '\n'.join(filtered_lines) 34 | 35 | def to_markdown(html, ignore=[], ignore_ids=[], ignore_classes=[], strip=[]): 36 | #html = html.encode('utf-8', 'replace').decode('utf-8') 37 | soup = BeautifulSoup(html, 'html.parser') 38 | 39 | # Remove elements based on tags 40 | for tag in ignore: 41 | for element in soup.find_all(tag): 42 | element.decompose() 43 | 44 | # Remove elements based on IDs 45 | for id_ in ignore_ids: 46 | for element in soup.find_all(id=id_): 47 | element.decompose() 48 | 49 | # Remove elements based on classes 50 | for class_ in ignore_classes: 51 | for element in soup.find_all(class_=class_): 52 | element.decompose() 53 | 54 | markdown = filter_markdown(md(str(soup), strip=strip)) 55 | return markdown 56 | 57 | -------------------------------------------------------------------------------- /openmacro/extensions/browser/utils/google.py: -------------------------------------------------------------------------------- 1 | async def get_events(self, page): 2 | classnames = { 3 | "title": "div.YOGjf", 4 | "location": "div.zvDXNd", 5 | "time": "div.SHrHx > div.cEZxRc:not(.zvDXNd)" 6 | } 7 | 8 | button = "div.ZFiwCf" 9 | expanded = "div.MmMIvd" if self.browser_type == "chrome" else "div.ZFiwCf" 10 | popup = "g-raised-button.Hg3NO" 11 | 12 | #await page.wait_for_selector(popup) 13 | #buttons = await page.query_selector_all(popup) 14 | #await buttons[1].click() 15 | 16 | await page.click(button) 17 | await page.wait_for_selector(expanded) 18 | 19 | keys = {key: None for key in classnames} 20 | events = [] 21 | for key, selector in classnames.items(): 22 | elements = await page.query_selector_all(selector) 23 | if events == []: 24 | events = [dict(keys) for _ in range(len(elements))] 25 | 26 | for index, elem in enumerate(elements): 27 | if key == "location" : 28 | if index % 2: # odd 29 | n = await elem.inner_text() 30 | events[index // 2][key] = temp + ', ' + n 31 | else: 32 | temp = await elem.inner_text() 33 | else: 34 | events[index][key] = await elem.inner_text() 35 | 36 | return events 37 | 38 | async def get_showtimes(self, page): 39 | classnames = { 40 | "venue": "div.YS9glc > div:not([class])", 41 | "location": "div.O4B9Zb" 42 | } 43 | 44 | container = "div.Evln0c" 45 | subcontainer = "div.iAkOed" 46 | plans = "div.swoqy" 47 | times_selector = "div.std-ts" 48 | 49 | keys = {key: None for key in classnames} 50 | events = [] 51 | for key, selector in classnames.items(): 52 | elements = await page.query_selector_all(selector) 53 | if events == []: 54 | events = [dict(keys) for _ in range(len(elements))] 55 | 56 | for index, elem in enumerate(elements): 57 | if key == 'location': 58 | location = await elem.inner_text() 59 | events[index][key] = location.replace("·", " away, at ") 60 | else: 61 | events[index][key] = await elem.inner_text() 62 | 63 | elements = await page.query_selector_all(container) 64 | for index, element in enumerate(elements): 65 | sub = await element.query_selector_all(subcontainer) 66 | for plan in sub: 67 | mode = await plan.query_selector(plans) 68 | 69 | if mode: mode_text = await mode.inner_text() 70 | else: mode_text = 'times' 71 | 72 | times = await plan.query_selector_all(times_selector) 73 | events[index][mode_text] = [await time.inner_text() for time in times] 74 | 75 | return events 76 | 77 | 78 | async def get_reviews(self, page): 79 | classnames = { 80 | "site": "span.rhsB", 81 | "rating": "span.gsrt" 82 | } 83 | 84 | rating_class = "div.xt8Uw" 85 | 86 | keys = {key: None for key in classnames} 87 | events = [] 88 | for key, selector in classnames.items(): 89 | elements = await page.query_selector_all(selector) 90 | if not events: 91 | events = [dict(keys) for _ in range(len(elements))] 92 | 93 | for index, elem in enumerate(elements): 94 | events[index][key] = await elem.inner_text() 95 | 96 | rating = await page.query_selector(rating_class) 97 | events.append({"site": "Google Reviews", "rating": await rating.inner_text() + "/5.0"}) 98 | 99 | return events 100 | 101 | async def get_weather(self, page): 102 | classnames = { 103 | "condition": "span#wob_dc", 104 | "time": "div#wob_dts", 105 | "temperature": "span#wob_tm", 106 | "unit": "div.wob-unit > span[style='display:inline']", 107 | "precipitation": "span#wob_pp", 108 | "humidity": "span#wob_hm", 109 | "wind": "span#wob_ws" 110 | } 111 | 112 | info = {key: None for key in classnames} 113 | for key, selector in classnames.items(): 114 | element = await page.query_selector(selector) 115 | info[key] = await element.inner_text() 116 | 117 | return info -------------------------------------------------------------------------------- /openmacro/extensions/email/__init__.py: -------------------------------------------------------------------------------- 1 | from email.mime.multipart import MIMEMultipart 2 | from email.mime.text import MIMEText 3 | from email.mime.base import MIMEBase 4 | from email import encoders 5 | from smtplib import SMTP 6 | import re 7 | 8 | from ...utils import ROOT_DIR 9 | from pathlib import Path 10 | 11 | from typing import TypedDict 12 | 13 | class EmailKwargs(TypedDict): 14 | email: str 15 | password: str 16 | 17 | def validate(email): 18 | pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$' 19 | if re.match(pattern, email): 20 | return email 21 | raise ValueError("Invalid email address") 22 | 23 | class Email: 24 | def __init__(self, email: str = None, password: str = None): 25 | 26 | special = {"yahoo.com": "smtp.mail.yahoo.com", 27 | "outlook.com": "smtp-mail.outlook.com", 28 | "hotmail.com": "smtp-mail.outlook.com", 29 | "icloud.com": "smtp.mail.me.com"} 30 | 31 | if email is None or password is None: 32 | raise KeyError(f"Missing credentials for `email` extension!\n{email} {password}") 33 | 34 | self.email = validate(email) 35 | self.password = password 36 | self.smtp_port = 587 37 | self.smtp_server = (special.get(server) 38 | if special.get(server := self.email.split("@")[1]) 39 | else "smtp." + server) 40 | 41 | @staticmethod 42 | def load_instructions(): 43 | with open(Path(ROOT_DIR, "extensions", "email", "docs", "instructions.md"), "r") as f: 44 | return f.read() 45 | 46 | def send(self, receiver_email, subject, body, attachments=[], cc=[], bcc=[]): 47 | msg = MIMEMultipart() 48 | 49 | try: 50 | msg['To'] = validate(receiver_email) 51 | except Exception as e: 52 | return {"status": f'Error: {e}'} 53 | 54 | msg['From'] = self.email 55 | msg['Subject'] = subject 56 | 57 | try: 58 | msg['Cc'] = ', '.join([validate(addr) for addr in cc]) 59 | except Exception as e: 60 | return {"status": f'Error: {e}'} 61 | 62 | msg.attach(MIMEText(body, 'plain')) 63 | 64 | for file in attachments: 65 | part = MIMEBase('application', 'octet-stream') 66 | part.set_payload(open(file, 'rb').read()) 67 | encoders.encode_base64(part) 68 | part.add_header('Content-Disposition', f'attachment; filename={file}') 69 | msg.attach(part) 70 | 71 | to_addrs = [receiver_email] + cc + bcc 72 | 73 | try: 74 | with SMTP(self.smtp_server, self.smtp_port) as server: 75 | server.starttls() 76 | server.login(self.email, self.password) 77 | text = msg.as_string() 78 | server.sendmail(self.email, to_addrs, text) 79 | return {"status": f"Email successfully sent to `{receiver_email}`!"} 80 | except Exception as e: 81 | return {"status": f'Error: {e}'} -------------------------------------------------------------------------------- /openmacro/extensions/email/docs/instructions.md: -------------------------------------------------------------------------------- 1 | # EMAIL APP 2 | email = Email() 3 | status = email.send("amor.budiyanto", title, content) # Sends an email to recipient 4 | print(status) # {"state": "success"} -------------------------------------------------------------------------------- /openmacro/extensions/extensions.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Openmacro/openmacro/746d3a1932fb60d34b3e543c4f297aef72e9514b/openmacro/extensions/extensions.txt -------------------------------------------------------------------------------- /openmacro/llm/__init__.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from .models.samba import SambaNova 3 | 4 | import os 5 | import re 6 | 7 | def interpret_input(input_str): 8 | pattern = r'```(?P\w+)\n(?P[\s\S]+?)```|(?P[^\n]+)' 9 | 10 | matches = re.finditer(pattern, input_str) 11 | 12 | blocks = [] 13 | current_message = None 14 | 15 | for match in matches: 16 | if match.group("format"): 17 | if current_message: 18 | blocks.append(current_message) 19 | current_message = None 20 | block = { 21 | "type": "code", 22 | "format": match.group("format"), 23 | "content": match.group("content").strip() 24 | } 25 | blocks.append(block) 26 | else: 27 | text = match.group("text").strip() 28 | if current_message: 29 | current_message["content"] += "\n" + text 30 | else: 31 | current_message = { 32 | "type": "message", 33 | "content": text 34 | } 35 | 36 | if current_message: 37 | blocks.append(current_message) 38 | 39 | return blocks 40 | 41 | def to_lmc(content: str, role: str = "assistant", type="message", format: str | None = None) -> dict: 42 | lmc = {"role": role, "type": type, "content": content} 43 | return lmc | ({} if format is None else {"format": format}) 44 | 45 | def to_chat(lmc: dict, logs=False) -> str: 46 | #_defaults = {} 47 | 48 | _type, _role, _content, _format = (lmc.get("type", "message"), 49 | lmc.get("role", "assistant"), 50 | lmc.get("content", "None"), 51 | lmc.get("format", None)) 52 | 53 | time = datetime.now().strftime("%I:%M %p %m/%d/%Y") 54 | 55 | if logs: 56 | return f'\033[90m({time})\033[0m \033[1m{_role}\033[0m: {_content}' 57 | 58 | if _role == "system": 59 | return ("----- SYSTEM PROMPT -----\n" + 60 | _content + "\n----- END SYSTEM PROMPT -----") 61 | 62 | return f'({time}) [type: {_type if _format is None else f"{_type}, format: {_format}"}] *{_role}*: {_content}' 63 | 64 | class LLM: 65 | def __init__(self, verbose=False, messages: list = None, system=""): 66 | self.system = system 67 | 68 | self.verbose = verbose 69 | 70 | if not (api_key := os.getenv('API_KEY')) : 71 | raise Exception("API_KEY for LLM not provided. Get yours for free from https://cloud.sambanova.ai/") 72 | 73 | self.llm = SambaNova(api_key=api_key, 74 | model="Meta-Llama-3.1-405B-Instruct", 75 | remember=True, 76 | system=self.system, 77 | messages=[] if messages is None else messages) 78 | self.messages = self.llm.messages 79 | 80 | def chat(self, *args, **kwargs): 81 | if self.system and not "system" in kwargs: 82 | return self.llm.chat(*args, **kwargs, system=self.system, max_tokens=1400) 83 | return self.llm.chat(*args, **kwargs, max_tokens=1400) 84 | -------------------------------------------------------------------------------- /openmacro/llm/models/samba.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | from random import choice 3 | from rich import print 4 | import aiohttp 5 | import asyncio 6 | import json 7 | 8 | def to_lmc(content: str, role: str = "assistant") -> dict: 9 | return {"role": role, "content": content} 10 | 11 | def available() -> set: 12 | return {"Meta-Llama-3.1-8B-Instruct", "Meta-Llama-3.1-70B-Instruct", "Meta-Llama-3.1-405B-Instruct", "Samba CoE", "Mistral-T5-7B-v1", "v1olet_merged_dpo_7B", "WestLake-7B-v2-laser-truthy-dpo", "DonutLM-v1", "SambaLingo Arabic", "SambaLingo Bulgarian", "SambaLingo Hungarian", "SambaLingo Russian", "SambaLingo Serbian (Cyrillic)", "SambaLingo Slovenian", "SambaLingo Thai", "SambaLingo Turkish", "SambaLingo Japanese"} 13 | 14 | class SambaNova: 15 | def __init__(self, 16 | api_key: str, 17 | model="Meta-Llama-3.1-8B-Instruct", 18 | messages=None, 19 | system="You are a helpful assistant.", 20 | remember=False, 21 | limit=30, 22 | endpoint= "https://api.sambanova.ai/v1/chat/completions"): 23 | 24 | if model in available(): 25 | self.model = model 26 | else: 27 | self.model = "Meta-Llama-3.1-8B-Instruct" 28 | 29 | self.api_key = api_key 30 | self.messages = [] if messages is None else messages 31 | self.remember = remember 32 | self.limit = limit 33 | self.system = to_lmc(system, role="system") 34 | self.endpoint = endpoint 35 | self.loop = asyncio.get_event_loop() 36 | 37 | async def async_stream_chat(self, data, remember=False): 38 | async with aiohttp.ClientSession() as session: 39 | async with session.post(self.endpoint, 40 | headers={"Authorization": f"Bearer {self.api_key}"}, 41 | json=data) as response: 42 | message = "" 43 | async for line in response.content: 44 | if line: 45 | decoded_line = line.decode('utf-8')[6:] 46 | if not decoded_line or decoded_line.strip() == "[DONE]": 47 | continue 48 | 49 | try: 50 | json_line = json.loads(decoded_line) 51 | except json.JSONDecodeError as e: 52 | print(line) 53 | raise json.JSONDecodeError(e) # better implementation for later 54 | 55 | if json_line.get("error"): 56 | yield json_line.get("error", {}).get("message", "An unexpected error occured!") 57 | 58 | options = json_line.get("choices", [{"finish_reason": "end_of_text"}])[0] 59 | if options.get("finish_reason") == "end_of_text": 60 | continue 61 | 62 | chunk = options.get('delta', {}).get('content', '') 63 | if self.remember or remember: 64 | message += chunk 65 | 66 | yield chunk 67 | 68 | if self.remember or remember: 69 | self.messages.append(to_lmc(message)) 70 | if (length := len(self.messages)) > self.limit: 71 | del self.messages[0] 72 | 73 | def chat(self, 74 | message: str, 75 | role="user", 76 | stream=False, 77 | max_tokens=1400, 78 | remember=False, 79 | lmc=False, 80 | asynchronous=True, 81 | system: str = None): 82 | 83 | system = to_lmc(system, role="system") if system else self.system 84 | if not lmc: 85 | message = to_lmc(message, role=role) 86 | elif message is None: 87 | message = self.messages[-1] 88 | self.messages = self.messages[:-1] 89 | 90 | template = {"model": self.model, 91 | "messages": [system] + self.messages + [message], 92 | "max_tokens": max_tokens, 93 | "stream": True} # lmao, we'll fix this later :p 94 | 95 | if self.remember or remember: 96 | self.messages.append(message) 97 | 98 | if stream: 99 | return self.async_stream_chat(template, remember) 100 | return self.loop.run_until_complete(self.static_chat(template, remember)) 101 | 102 | async def static_chat(self, template, remember): 103 | return "".join([chunk 104 | async for chunk in 105 | self.async_stream_chat(template, remember)]) 106 | 107 | async def main(): 108 | llm = SambaNova("APIKEY", 109 | remember=True) 110 | while True: 111 | async for chunk in llm.chat(input("message: "), stream=True): 112 | print(chunk, end="") 113 | 114 | if __name__ == "__main__": 115 | asyncio.run(main()) 116 | -------------------------------------------------------------------------------- /openmacro/memory/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Openmacro/openmacro/746d3a1932fb60d34b3e543c4f297aef72e9514b/openmacro/memory/__init__.py -------------------------------------------------------------------------------- /openmacro/memory/client.py: -------------------------------------------------------------------------------- 1 | from chromadb import HttpClient, AsyncHttpClient 2 | 3 | import logging 4 | logging.disable(logging.CRITICAL + 1) 5 | 6 | Memory: HttpClient = HttpClient 7 | AsyncMemory: AsyncHttpClient = AsyncHttpClient 8 | 9 | # can't believe HttpClient and AsyncHttpClient are functions, not classes 10 | # class Memory(HttpClient): 11 | # def __init__(self, host: str = None, port: int = None, telemetry: bool = False): 12 | # self.client = HttpClient(host=host or "localhost", 13 | # port=port or 8000, 14 | # settings=Settings(anonymized_telemetry=telemetry)) 15 | 16 | # class AsyncMemory(AsyncHttpClient): 17 | # def __init__(self, host: str = None, port: int = None, telemetry: bool = False): 18 | # super().__init__(host=host or "localhost", 19 | # port=port or 8000, 20 | # settings=Settings(anonymized_telemetry=telemetry)) 21 | -------------------------------------------------------------------------------- /openmacro/memory/server.py: -------------------------------------------------------------------------------- 1 | import subprocess 2 | from pathlib import Path 3 | import chromadb 4 | from chromadb.config import Settings 5 | 6 | class Manager: 7 | def __init__(self, 8 | path: Path | str = None, 9 | port: int = None, 10 | collections: list[str] = None, 11 | telemetry: bool = False): 12 | self.port = port or 8000 13 | self.path = path 14 | self.collections = collections or ["ltm", "cache"] 15 | self.process = None 16 | 17 | if not Path(path).is_dir(): 18 | client = chromadb.PersistentClient(str(path), Settings(anonymized_telemetry=telemetry)) 19 | for collection in self.collections: 20 | client.create_collection(name=collection) 21 | 22 | def serve(self): 23 | self.process = subprocess.Popen( 24 | ["chroma", "run", "--path", str(self.path), "--port", str(self.port)], 25 | stdout=subprocess.PIPE, 26 | stderr=subprocess.PIPE, 27 | text=True 28 | ) 29 | 30 | def serve_and_wait(self): 31 | self.serve() 32 | for line in iter(self.process.stdout.readline, ''): 33 | # print(line, end='') 34 | if f"running on http://localhost:{self.port}" in line: 35 | break 36 | -------------------------------------------------------------------------------- /openmacro/omi/__init__.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | from ..computer import Computer 3 | from ..utils import ROOT_DIR 4 | from pathlib import Path 5 | import subprocess 6 | from rich_argparse import RichHelpFormatter 7 | 8 | def main(): 9 | RichHelpFormatter.styles["argparse.groups"] = "bold" 10 | RichHelpFormatter.styles["argparse.args"] = "#79c0ff" 11 | RichHelpFormatter.styles["argparse.metavar"] = "#2a6284" 12 | 13 | parser = argparse.ArgumentParser( 14 | description="[#92c7f5]o[/#92c7f5][#8db9fe]m[/#8db9fe][#9ca4eb]i[/#9ca4eb] is the package manager for openmacro. [dim](0.0.1)[/dim]", 15 | formatter_class=RichHelpFormatter 16 | ) 17 | 18 | subparsers = parser.add_subparsers(dest='command', help='Sub-command help') 19 | 20 | install_parser = subparsers.add_parser('install', help='Install a module through `pip` and add it to openmacro path') 21 | install_parser.add_argument('module_name', metavar='', type=str, help='Module name to install') 22 | 23 | add_parser = subparsers.add_parser('add', help='Add extension to `openmacro.extensions` path') 24 | add_parser.add_argument('module_name', metavar='', type=str, help='Module name to add') 25 | 26 | remove_parser = subparsers.add_parser('remove', help='Remove extension from `openmacro.extensions` path') 27 | remove_parser.add_argument('module_name', metavar='', type=str, help='Module name to remove') 28 | 29 | args = parser.parse_args() 30 | pip = [Computer().supported["python"][0], "-m", "pip", "install"] 31 | 32 | if args.command == 'install': 33 | subprocess.run(pip + [args.module_name]) 34 | with open(Path(ROOT_DIR, "extensions", "extensions.txt"), "a") as f: 35 | f.write("\n" + args.module_name) 36 | 37 | elif args.command == 'add': 38 | with open(Path(ROOT_DIR, "extensions", "extensions.txt"), "a") as f: 39 | f.write("\n" + args.module_name) 40 | 41 | elif args.command == 'remove': 42 | with open(Path(ROOT_DIR, "extensions", "extensions.txt"), "r") as f: 43 | extensions = f.read().splitlines() 44 | 45 | extensions = [ext for ext in extensions if ext != args.module_name] 46 | 47 | with open(Path(ROOT_DIR, "extensions", "extensions.txt"), "w") as f: 48 | f.write("\n".join(extensions)) 49 | 50 | if __name__ == "__main__": 51 | main() 52 | -------------------------------------------------------------------------------- /openmacro/profile/__init__.py: -------------------------------------------------------------------------------- 1 | from typing import TypedDict, List, Dict 2 | 3 | class User(TypedDict): 4 | name: str 5 | version: str 6 | 7 | class Assistant(TypedDict): 8 | name: str 9 | personality: str 10 | messages: List[str] 11 | local: bool 12 | breakers: List[str] 13 | 14 | class Safeguards(TypedDict): 15 | timeout: int 16 | auto_run: bool 17 | auto_install: bool 18 | 19 | class Paths(TypedDict): 20 | prompts: str 21 | memories: str 22 | 23 | class Config(TypedDict): 24 | telemetry: bool 25 | ephemeral: bool 26 | verbose: bool 27 | 28 | class Profile(TypedDict): 29 | user: User 30 | assistant: Assistant 31 | safeguards: Safeguards 32 | paths: Paths 33 | extensions: Dict 34 | config: Config -------------------------------------------------------------------------------- /openmacro/profile/template.py: -------------------------------------------------------------------------------- 1 | from openmacro.profile import Profile 2 | from openmacro.utils import USERNAME, ROOT_DIR 3 | from openmacro.extensions import BrowserKwargs 4 | from pathlib import Path 5 | 6 | profile: Profile = Profile( 7 | user = { 8 | "name": USERNAME, 9 | "version": "1.0.0" 10 | }, 11 | assistant = { 12 | "name": "Macro", 13 | "personality": "You respond in a professional attitude and respond in a formal, yet casual manner.", 14 | "messages": [], 15 | "breakers": ["the task is done.", 16 | "the conversation is done."] 17 | }, 18 | safeguards = { 19 | "timeout": 16, 20 | "auto_run": True, 21 | "auto_install": True 22 | }, 23 | paths = { 24 | "prompts": Path(ROOT_DIR, "core", "prompts"), 25 | }, 26 | config = { 27 | "telemetry": False, 28 | "ephemeral": False, 29 | "verbose": True, 30 | "local": False, 31 | "dev": False, 32 | "conversational": True, 33 | }, 34 | extensions = { 35 | "Browser": BrowserKwargs(engine="google") 36 | }, 37 | tts = { 38 | "enabled": True, 39 | "engine": "SystemEngine" 40 | }, 41 | env = { 42 | 43 | } 44 | ) -------------------------------------------------------------------------------- /openmacro/speech/__init__.py: -------------------------------------------------------------------------------- 1 | from ..utils import lazy_import 2 | 3 | class Speech: 4 | def __init__(self, 5 | tts: dict = None, 6 | stt: dict = None) -> None: 7 | config = stt or {} 8 | if config.get("enabled"): 9 | try: 10 | from .stt import STT 11 | self.stt = STT(config, config.get("engine", "SystemEngine")) 12 | except: 13 | print("An error occured: Disabling STT.") 14 | 15 | config = tts or {} 16 | if config.get("enabled"): 17 | # try: 18 | from .tts import TTS 19 | self.tts = TTS(config, config.get("engine", "SystemEngine")) 20 | # except: 21 | # print("An error occured: Disabling TTS.") 22 | -------------------------------------------------------------------------------- /openmacro/speech/stt.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Openmacro/openmacro/746d3a1932fb60d34b3e543c4f297aef72e9514b/openmacro/speech/stt.py -------------------------------------------------------------------------------- /openmacro/speech/tts.py: -------------------------------------------------------------------------------- 1 | from ..utils import lazy_import 2 | import os 3 | 4 | RealtimeTTS = lazy_import("realtimetts", 5 | "RealtimeTTS", 6 | "realtimetts[system,gtts,elevenlabs]", 7 | optional=False, 8 | verbose=False, 9 | install=True) 10 | 11 | import logging 12 | logging.disable(logging.CRITICAL + 1) 13 | 14 | def setup(engine: str, api_key: str = None, voice: str = None): 15 | free = {"SystemEngine", "GTTSEngine"} 16 | paid = {"ElevenlabsEngine": "ELEVENLABS_API_KEY", 17 | "OpenAIEngine": "OPENAI_API_KEY"} 18 | supported = free | set(paid) 19 | 20 | if not engine in supported: 21 | raise ValueError(f"Engine not supported in following: {supported}") 22 | 23 | if engine in free: 24 | return {} 25 | 26 | if api_key is None: 27 | raise ValueError(f"API_KEY not specified") 28 | 29 | os.environ[paid[engine]] = api_key 30 | return {"voice": voice} if voice else {} 31 | 32 | 33 | class TTS(RealtimeTTS.TextToAudioStream): 34 | def __init__(self, 35 | tts: dict = None, 36 | engine: str = None, 37 | api_key: str = None, 38 | voice: str = None, 39 | *args, **kwargs): 40 | self.config = tts or {} 41 | 42 | engine_kwargs = setup(engine, 43 | api_key or self.config.get("api_key"), 44 | voice or self.config.get("voice")) 45 | 46 | self.engine = getattr(RealtimeTTS, engine)(**engine_kwargs) 47 | super().__init__(self.engine, *args, **kwargs) 48 | self.chunks = "" 49 | 50 | def stream(self, chunk): 51 | if chunk == "": 52 | self.feed(self.chunks) 53 | self.chunks = "" 54 | self.play_async() 55 | else: 56 | self.chunks += chunk -------------------------------------------------------------------------------- /openmacro/tests/__init__.py: -------------------------------------------------------------------------------- 1 | from ..core import Openmacro 2 | from rich import print 3 | import asyncio 4 | import os 5 | 6 | os.environ["API_KEY"] = "e8e85d70-74cd-43f7-bd5e-fd8dec181037" 7 | macro = Openmacro() 8 | 9 | def browser(): 10 | query = input("search: ") 11 | summary = macro.extensions.browser.search(query, n=1) 12 | print(summary) 13 | 14 | # input("Press enter to continue...") 15 | # results = get_relevant(macro.collection.query(query_texts=[query], n_results=3)) 16 | # for document in results['documents'][0]: 17 | # print(Markdown(document)) 18 | # print("\n\n") 19 | 20 | def perplexity(): 21 | query = input("search: ") 22 | summary = macro.extensions.browser.perplexity_search(query) 23 | print(summary) 24 | 25 | # input("Press enter to continue...") 26 | # results = get_relevant(macro.collection.query(query_texts=[query], n_results=3)) 27 | # for document in results['documents'][0]: 28 | # print(Markdown(document)) 29 | # print("\n\n") 30 | 31 | perplexity() 32 | #browser() -------------------------------------------------------------------------------- /openmacro/tests/cases/browser.py: -------------------------------------------------------------------------------- 1 | def test(): 2 | from ...openmacro import computer 3 | code = "extensions.browser.search('externalities in economics', n=1)" 4 | results = computer.run_python(code) 5 | 6 | print(results) 7 | 8 | test() -------------------------------------------------------------------------------- /openmacro/utils/__init__.py: -------------------------------------------------------------------------------- 1 | import subprocess 2 | import importlib.util 3 | 4 | import getpass 5 | 6 | from pathlib import Path 7 | import platform 8 | import sys 9 | import os 10 | import re 11 | 12 | import random 13 | import string 14 | import toml 15 | 16 | # constants 17 | ROOT_DIR = Path(__file__).resolve().parent.parent 18 | PLATFORM = platform.uname() 19 | USERNAME = getpass.getuser() 20 | SYSTEM = platform.system() 21 | OS = f"{SYSTEM} {platform.version()}" 22 | 23 | def env_safe_replace(path: Path | str, 24 | variables: dict): 25 | 26 | if not Path(path).is_file(): 27 | raise FileNotFoundError("Path to `.env` not found.") 28 | 29 | with open(path, "r") as f: 30 | env = toml.load(f) 31 | 32 | with open(path, "w") as f: 33 | f.write(toml.dumps( 34 | env | variables 35 | )) 36 | 37 | for key, value in variables.items(): 38 | os.environ[key] = value 39 | 40 | def is_installed(package): 41 | spec = importlib.util.find_spec(package) 42 | return spec is not None 43 | 44 | def python_load_profile(path: Path | str): 45 | module_name = f"openmacro.parse_profile" 46 | spec = importlib.util.spec_from_file_location(module_name, path) 47 | module = importlib.util.module_from_spec(spec) 48 | 49 | sys.modules[module_name] = module 50 | spec.loader.exec_module(module) 51 | 52 | return getattr(module, "profile", {}) 53 | 54 | def init_profile(profile: dict, 55 | memories_dir: Path | str): 56 | 57 | name, version = profile["user"]["name"], profile["user"]["version"] 58 | 59 | # if profile already initialised 60 | if Path(memories_dir).is_dir(): 61 | if os.getenv("PROFILE") == f"{name}:{version}": 62 | return 63 | 64 | # collision 65 | override = input("Another profile with the same name and version already exists. " + 66 | "Override profile? (y/n)").startswith("y") 67 | if not override: 68 | raise FileExistsError("profile with the same name and version already exists") 69 | 70 | memories_dir.mkdir(parents=True, exist_ok=True) 71 | original_profile_path = str(profile["env"].get("path", "")) 72 | 73 | # clean paths 74 | profile["env"]["path"] = original_profile_path 75 | for key, path in profile["paths"].items(): 76 | profile["paths"][key] = str(path) 77 | 78 | path = Path(memories_dir, "profile.json") 79 | path.touch() 80 | 81 | json = lazy_import("json") 82 | with open(path, "w") as f: 83 | f.write(json.dumps(profile)) 84 | 85 | env = Path(memories_dir.parent, ".env") 86 | env.touch() 87 | 88 | api_key = profile["env"].get("api_key") or os.getenv("API_KEY") 89 | if not api_key: 90 | raise ValueError("API_KEY for LLM not provided. Get yours for free from https://cloud.sambanova.ai/") 91 | 92 | # fall-back 93 | if not os.getenv("API_KEY"): 94 | env_safe_replace(Path(ROOT_DIR, ".env"), 95 | {"API_KEY": api_key}) 96 | 97 | env_safe_replace(env, 98 | {"ORIGINAL_PROFILE_PATH": original_profile_path, 99 | "API_KEY": api_key}) 100 | 101 | env_safe_replace(Path(ROOT_DIR, ".env"), 102 | {"PROFILE": f"{name}:{version}"}) 103 | 104 | os.environ["PROFILE"] = f"{name}:{version}" 105 | 106 | 107 | def load_profile(profile_path: Path | str, 108 | strict=False): 109 | if profile_path is None: 110 | return {} 111 | 112 | profile_path = Path(profile_path) 113 | if not profile_path.is_file(): 114 | if strict: 115 | raise FileNotFoundError(f"Path to `{profile_path}` does not exist") 116 | return {} 117 | 118 | suffix = profile_path.suffix.lower() 119 | with open(profile_path, "r") as f: 120 | if suffix == ".json": 121 | return lazy_import("json").load(f) 122 | elif suffix in {".yaml", ".yml"}: 123 | return lazy_import("yaml").safe_load(f) 124 | elif suffix == ".toml": 125 | return lazy_import("toml").load(f) 126 | elif suffix in {".py", ".pyw"}: 127 | return python_load_profile(profile_path) 128 | 129 | return {} 130 | 131 | def re_format(text, replacements, pattern=r'\{([a-zA-Z0-9_]+)\}', strict=False): 132 | matches = set(re.findall(pattern, text)) 133 | if strict and (missing := matches - set(replacements.keys())): 134 | raise ValueError(f"Missing replacements for: {', '.join(missing)}") 135 | 136 | for match in matches & set(replacements.keys()): 137 | text = re.sub(r'\{' + match + r'\}', str(replacements[match]), text) 138 | return text 139 | 140 | def load_prompts(dir, 141 | info: dict = {}, 142 | conversational: bool = False): 143 | prompts = {} 144 | 145 | for filename in Path(dir).iterdir(): 146 | if not filename.is_file(): 147 | continue 148 | 149 | name = filename.stem 150 | with open(Path(dir, filename), "r") as f: 151 | prompts[name] = re_format(f.read().strip(), info) 152 | 153 | prompts['initial'] += "\n\n" + prompts['instructions'] 154 | if conversational: 155 | prompts['initial'] += "\n\n" + prompts['conversational'] 156 | 157 | return prompts 158 | 159 | def Kwargs(**kwargs): 160 | return kwargs 161 | 162 | def lazy_import(package, 163 | name: str = '', 164 | install_name: str = '', 165 | prefixes: tuple = (("pip", "install"), 166 | ("py", "-m", "pip", "install")), 167 | scripts: list | tuple = [], 168 | install= False, 169 | void = False, 170 | verbose = False, 171 | optional=False): 172 | 173 | name = name or package 174 | if package in sys.modules: 175 | return sys.modules[package] 176 | 177 | spec = importlib.util.find_spec(name or package) 178 | if spec is None: 179 | if optional: 180 | return None 181 | elif install: 182 | if verbose: 183 | print(f"Module '{package}' is missing, proceeding to install.") 184 | 185 | for prefix in prefixes: 186 | try: 187 | result = subprocess.run(prefix + (install_name or package,), 188 | shell=True, 189 | capture_output=True) 190 | break 191 | except subprocess.CalledProcessError: 192 | continue 193 | if result.returncode: 194 | raise ImportError(f"Failed to install module '{name}'") 195 | 196 | for script in scripts: 197 | try: 198 | result = subprocess.run(script, shell=True, capture_output=True) 199 | except subprocess.CalledProcessError: 200 | continue 201 | 202 | spec = importlib.util.find_spec(name) 203 | if spec is None: 204 | raise ImportError(f"Failed to install module '{name}'") 205 | else: 206 | raise ImportError(f"Module '{name}' cannot be found") 207 | elif verbose: 208 | print(f"Module '{name}' is already installed.") 209 | 210 | if void: 211 | return None 212 | 213 | if not install: 214 | loader = importlib.util.LazyLoader(spec.loader) 215 | spec.loader = loader 216 | 217 | module = importlib.util.module_from_spec(spec) 218 | sys.modules[name or package] = module 219 | 220 | if install: 221 | spec.loader.exec_module(module) 222 | 223 | importlib.reload(module) 224 | 225 | return module 226 | 227 | def lazy_imports(packages: list[str | tuple[str]], 228 | prefix: str = "pip install ", 229 | user_install = False, 230 | void = False): 231 | 232 | libs = [] 233 | for package in packages: 234 | if not isinstance(package, str): 235 | package = (package[0], None) if len(package) == 1 else package 236 | else: 237 | package = (package, None) 238 | 239 | if (pack := lazy_import(*package, prefix=prefix, user_install=user_install, void=void)): 240 | libs.append(pack) 241 | 242 | if void: 243 | return None 244 | 245 | return tuple(libs) 246 | 247 | def merge_dicts(dict1, dict2): 248 | for key, value in dict2.items(): 249 | if key in dict1: 250 | if isinstance(value, dict) and isinstance(dict1[key], dict): 251 | merge_dicts(dict1[key], value) 252 | else: 253 | dict1[key] = value 254 | else: 255 | dict1[key] = value 256 | return dict1 257 | 258 | def generate_id(length=8): 259 | characters = string.ascii_letters + string.digits 260 | return ''.join(random.choice(characters) for _ in range(length)) 261 | 262 | def get_relevant(document: dict, threshold: float = 1.125, clean=False): 263 | # temp, filter by distance 264 | # future, density based retrieval relevance 265 | # https://github.com/chroma-core/chroma/blob/main/chromadb/experimental/density_relevance.ipynb 266 | 267 | np = lazy_import("numpy", 268 | install=True, 269 | optional=False) 270 | 271 | mask = np.array(document['distances']) <= threshold 272 | keys = tuple(set(document) & set(('distances', 'documents', 'metadatas', 'ids'))) 273 | for key in keys: 274 | document[key] = np.array(document[key])[mask].tolist() 275 | 276 | if document.get('ids'): 277 | _, unique_indices = np.unique(document['ids'], return_index=True) 278 | for key in ('distances', 'documents', 'metadatas', 'ids'): 279 | document[key] = np.array(document[key])[unique_indices].tolist() 280 | 281 | if clean: 282 | document = "\n\n".join(np.array(document["documents"]).flatten().tolist()) 283 | return document -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["poetry-core>=1.0.0"] 3 | build-backend = "poetry.core.masonry.api" 4 | 5 | [tool.poetry] 6 | name = "openmacro" 7 | version = "0.2.11" 8 | description = "Multimodal Assistant. Human Interface for computers." 9 | authors = ["Amor Budiyanto "] 10 | license = "MIT" 11 | readme = "README.md" 12 | homepage = "https://github.com/amooo-ooo/openmacro" 13 | 14 | [tool.poetry.dependencies] 15 | python = "^3.8,<4.0" 16 | toml = ">=0.10" 17 | rich = ">=10.0" 18 | playwright="*" 19 | asyncio="*" 20 | markdownify="*" 21 | beautifulsoup4="*" 22 | pybrowsers="*" 23 | chromadb="*" 24 | rich-argparse="*" 25 | 26 | [tool.poetry.scripts] 27 | macro = "openmacro.__main__:main" 28 | omi = "openmacro.omi:main" 29 | 30 | [tool.poetry.extras] 31 | dev = ["pytest"] 32 | --------------------------------------------------------------------------------