├── .gitattributes ├── .github └── workflows │ └── pylint.yml ├── .gitignore ├── LICENSE ├── README.md ├── docs ├── images │ ├── demo.png │ └── mindflow-title.svg └── videos │ └── demo.mov ├── mindflow ├── __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 | # MindFlow 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 | # MindFlow 2 | 3 |
4 | License 5 | Commit Activity 6 | Last Commit 7 | Stars 8 |
9 | 10 | MindFlow is an advanced multimodal personal agent designed to harness the power of Large Language Models (LLMs) for local code execution. As your autonomous digital assistant, MindFlow can complete and automate a wide range of tasks, from simple to complex, through self-prompting and natural language commands. 11 | 12 | ## Features 13 | 14 | MindFlow provides a command-line interface for natural language interaction, allowing you to: 15 | 16 | - Automate and complete various tasks. 17 | - Analyze and visualize data. 18 | - Access and browse the web for up-to-date information. 19 | - Manipulate a variety of file types, including photos, videos, PDFs, and more. 20 | 21 | Currently, MindFlow supports API keys for models powered by SambaNova. This choice is due to SambaNova's free, fast, and reliable service, which is ideal for the project's testing phase. Future versions will include support for other providers such as OpenAI and Anthropic. 22 | 23 | This project draws significant inspiration from [`Open Interpreter`](https://github.com/OpenInterpreter/open-interpreter) ❤️ 24 | 25 | ## Getting Started 26 | 27 | To begin using MindFlow, obtain a free API key by creating an account at [https://cloud.sambanova.ai/](https://cloud.sambanova.ai/). 28 | 29 | Next, install and start MindFlow with the following commands: 30 | 31 | ```shell 32 | pip install mindflow 33 | mindflow --api_key "YOUR_API_KEY" 34 | ``` 35 | 36 | > [!TIP] 37 | > Encountering issues? Report them [here](https://github.com/amooo-ooo/MindFlow/issues/new) or try: 38 | 39 | ```shell 40 | py -m pip install mindflow 41 | py -m mindflow --api_key "YOUR_API_KEY" 42 | ``` 43 | 44 | > [!NOTE] 45 | > The `--api_key` parameter is only needed once. For subsequent uses, simply run `mindflow` or `py -m mindflow`. 46 | 47 | > [!TIP] 48 | > Different API keys can be assigned to different profiles: 49 | 50 | ```shell 51 | py -m mindflow --api_key "YOUR_API_KEY" --profile "path\to\profile" 52 | ``` 53 | 54 | ## Profiles 55 | 56 | MindFlow supports command-line arguments and customizable settings. View argument options with: 57 | 58 | ```shell 59 | mindflow --help 60 | ``` 61 | 62 | To create and save personalized settings for future use: 63 | 64 | ```shell 65 | mindflow --profile "path\to\profile" 66 | ``` 67 | 68 | MindFlow supports profiles in `JSON`, `TOML`, `YAML`, and `Python` formats. Below are examples of each format. 69 | 70 |
71 | Python 72 | 73 | Example `profile.py`: 74 | 75 | ```python 76 | from mindflow.profile import Profile 77 | from mindflow.extensions import BrowserKwargs, EmailKwargs 78 | 79 | profile: Profile = Profile( 80 | user = { 81 | "name": "Run-Tu", 82 | "version": "1.0.0" 83 | }, 84 | assistant = { 85 | "name": "MindFlow", 86 | "personality": "You respond in a professional attitude and respond in a formal, yet casual manner.", 87 | "messages": [], 88 | "breakers": ["the task is done.", "the conversation is done."] 89 | }, 90 | safeguards = { 91 | "timeout": 16, 92 | "auto_run": True, 93 | "auto_install": True 94 | }, 95 | extensions = { 96 | "Browser": BrowserKwargs(headless=False, engine="google"), 97 | "Email": EmailKwargs(email="run-tu@example.com", password="password") 98 | }, 99 | config = { 100 | "verbose": True, 101 | "conversational": True, 102 | "dev": False 103 | }, 104 | languages = { 105 | "python": ["C:\Windows\py.EXE", "-c"], 106 | "rust": ["cargo", "script", "-e"] 107 | }, 108 | tts = { 109 | "enabled": True, 110 | "engine": "OpenAIEngine", 111 | "api_key": "sk-example" 112 | } 113 | ) 114 | ``` 115 | 116 | To extend MindFlow and build your own app: 117 | 118 | ```python 119 | async def main(): 120 | from mindflow.core import MindFlowCore 121 | 122 | mindflow = MindFlowCore(profile) 123 | mindflow.llm.messages = [] 124 | 125 | async for chunk in mindflow.chat("Plot an exponential graph for me!", stream=True): 126 | print(chunk, end="") 127 | 128 | import asyncio 129 | asyncio.run(main) 130 | ``` 131 | 132 |
133 | 134 |
135 | JSON 136 | 137 | Example `profile.json`: 138 | 139 | ```json 140 | { 141 | "user": { 142 | "name": "Run-Tu", 143 | "version": "1.0.0" 144 | }, 145 | "assistant": { 146 | "name": "MindFlow", 147 | "personality": "You respond in a professional attitude and respond in a formal, yet casual manner.", 148 | "messages": [], 149 | "breakers": ["the task is done.", "the conversation is done."] 150 | }, 151 | "safeguards": { 152 | "timeout": 16, 153 | "auto_run": true, 154 | "auto_install": true 155 | }, 156 | "extensions": { 157 | "Browser": { 158 | "headless": false, 159 | "engine": "google" 160 | }, 161 | "Email": { 162 | "email": "run-tu@example.com", 163 | "password": "password" 164 | } 165 | }, 166 | "config": { 167 | "verbose": true, 168 | "conversational": true, 169 | "dev": false 170 | }, 171 | "languages": { 172 | "python": ["C:\\Windows\\py.EXE", "-c"], 173 | "rust": ["cargo", "script", "-e"] 174 | }, 175 | "tts": { 176 | "enabled": true, 177 | "engine": "OpenAIEngine", 178 | "api_key": "sk-example" 179 | } 180 | } 181 | ``` 182 | 183 |
184 | 185 |
186 | TOML 187 | 188 | Example `profile.toml`: 189 | 190 | ```toml 191 | [user] 192 | name = "Run-Tu" 193 | version = "1.0.0" 194 | 195 | [assistant] 196 | name = "MindFlow" 197 | personality = "You respond in a professional attitude and respond in a formal, yet casual manner." 198 | messages = [] 199 | breakers = ["the task is done.", "the conversation is done."] 200 | 201 | [safeguards] 202 | timeout = 16 203 | auto_run = true 204 | auto_install = true 205 | 206 | [extensions.Browser] 207 | headless = false 208 | engine = "google" 209 | 210 | [extensions.Email] 211 | email = "run-tu@example.com" 212 | password = "password" 213 | 214 | [config] 215 | verbose = true 216 | conversational = true 217 | dev = false 218 | 219 | [languages] 220 | python = ["C:\\Windows\\py.EXE", "-c"] 221 | rust = ["cargo", "script", "-e"] 222 | 223 | [tts] 224 | enabled = true 225 | engine = "OpenAIEngine" 226 | api_key = "sk-example" 227 | ``` 228 | 229 |
230 | 231 |
232 | YAML 233 | 234 | Example `profile.yaml`: 235 | 236 | ```yaml 237 | user: 238 | name: "Run-Tu" 239 | version: "1.0.0" 240 | 241 | assistant: 242 | name: "MindFlow" 243 | personality: "You respond in a professional attitude 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: "run-tu@example.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: "OpenAIEngine" 274 | api_key: "sk-example" 275 | ``` 276 | 277 |
278 | 279 | To switch between profiles, use: 280 | 281 | ```shell 282 | mindflow --switch "run-tu" 283 | ``` 284 | 285 | Profiles also support versioning: 286 | 287 | ```shell 288 | mindflow --switch "run-tu:1.0.0" 289 | ``` 290 | 291 | > [!NOTE] 292 | > All profiles are isolated. LTM from different profiles and versions are not shared. 293 | 294 | For quick profile updates `[BETA]`: 295 | 296 | ```shell 297 | mindflow --update "run-tu" 298 | ``` 299 | 300 | To view all available profiles: 301 | 302 | ```shell 303 | mindflow --profiles 304 | ``` 305 | 306 | To view all profile versions: 307 | 308 | ```shell 309 | mindflow --versions 310 | ``` 311 | 312 | ## Extensions 313 | 314 | MindFlow supports custom RAG extensions for enhanced capabilities. The default extensions include `browser` and `email`. 315 | 316 | ### Writing Extensions 317 | 318 | Create extensions using the following template: 319 | 320 | ```python 321 | from typing import TypedDict 322 | 323 | class ExtensionKwargs(TypedDict): 324 | ... 325 | 326 | class ExtensionName: 327 | def __init__(self): 328 | ... 329 | 330 | @staticmethod 331 | def load_instructions() -> str: 332 | return "" 333 | ``` 334 | 335 | If your extension does not include a kwarg class, use: 336 | 337 | ```python 338 | from mindflow.utils import Kwargs 339 | ``` 340 | 341 | Upload your code to `pypi` using `twine` and `poetry`. To add it to `mindflow.extensions` for profiles: 342 | 343 | ```shell 344 | omi install 345 | ``` 346 | 347 | or 348 | 349 | ```shell 350 | pip install 351 | omi add 352 | ``` 353 | 354 | To test your extensions locally: 355 | 356 | ```shell 357 | omi install . 358 | ``` 359 | 360 | ## Todo List 361 | 362 | - [x] AI Interpreter 363 | - [X] Web Search Capability 364 | - [X] Async Chunk Streaming 365 | - [X] API Keys Support 366 | - [X] Profiles Support 367 | - [X] Extensions API 368 | - [ ] Semantic File Search 369 | - [ ] Optional Telemetry 370 | - [ ] Desktop, Android & iOS App Interface 371 | 372 | ## Current Focus 373 | 374 | - Optimizations 375 | - Cost-efficient long-term memory and conversational context managers using vector databases, likely powered by [`ChromaDB`](https://github.com/chroma-core/chroma). 376 | - Hooks API and Live Code Output Streaming 377 | 378 | ## Contributions 379 | 380 | As my first major open-source project, there may be challenges and plenty of room for improvement. Contributions are welcome, whether through raising issues, improving documentation, adding comments, or suggesting features. Your support is greatly appreciated! 381 | 382 | ## Support 383 | 384 | You can support this project by writing custom extensions for MindFlow. The aim is to be community-powered, with its capabilities expanding through collective effort. More extensions mean better handling of complex tasks. An official list of verified MindFlow extensions will be created in the future. 385 | -------------------------------------------------------------------------------- /docs/images/demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Run-Tu/MindFlow/3d3fdee8833236f3b1b459061b33bb13b59f5ee6/docs/images/demo.png -------------------------------------------------------------------------------- /docs/images/mindflow-title.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /docs/videos/demo.mov: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Run-Tu/MindFlow/3d3fdee8833236f3b1b459061b33bb13b59f5ee6/docs/videos/demo.mov -------------------------------------------------------------------------------- /mindflow/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Run-Tu/MindFlow/3d3fdee8833236f3b1b459061b33bb13b59f5ee6/mindflow/__init__.py -------------------------------------------------------------------------------- /mindflow/__main__.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | from .core import MindFlow 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 mindflow.") 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 = MindFlow(profile) 144 | asyncio.run(run_cli(macro)) 145 | 146 | if __name__ == "__main__": 147 | main() 148 | -------------------------------------------------------------------------------- /mindflow/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 `mindflow`...") 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") 64 | -------------------------------------------------------------------------------- /mindflow/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 mindflow.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"MindFlow 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 | -------------------------------------------------------------------------------- /mindflow/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 MindFlow: 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("MindFlow 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)) 249 | -------------------------------------------------------------------------------- /mindflow/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 mindflow.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. 21 | -------------------------------------------------------------------------------- /mindflow/core/prompts/initial.txt: -------------------------------------------------------------------------------- 1 | You are {assistant}, a world-class assistant working under MindFlow that can complete any goal by executing code. 2 | 3 | User's Name: {username} 4 | User's OS: {os} 5 | 6 | -------------------------------------------------------------------------------- /mindflow/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 mindflow.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 mindflow.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} 57 | -------------------------------------------------------------------------------- /mindflow/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 | {} 24 | -------------------------------------------------------------------------------- /mindflow/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() 19 | -------------------------------------------------------------------------------- /mindflow/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 mindflow 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 mindflow 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 | -------------------------------------------------------------------------------- /mindflow/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"] 8 | -------------------------------------------------------------------------------- /mindflow/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 6 | -------------------------------------------------------------------------------- /mindflow/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 | } 17 | -------------------------------------------------------------------------------- /mindflow/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 9 | -------------------------------------------------------------------------------- /mindflow/extensions/browser/tests/tests.py: -------------------------------------------------------------------------------- 1 | from .. import Browser 2 | 3 | browser = Browser(headless=False) 4 | print(browser.search("Inside Out 2")) 5 | -------------------------------------------------------------------------------- /mindflow/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 | -------------------------------------------------------------------------------- /mindflow/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 118 | -------------------------------------------------------------------------------- /mindflow/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}'} 82 | -------------------------------------------------------------------------------- /mindflow/extensions/email/docs/instructions.md: -------------------------------------------------------------------------------- 1 | # EMAIL APP 2 | email = Email() 3 | status = email.send("run-tu", title, content) # Sends an email to recipient 4 | print(status) # {"state": "success"} 5 | -------------------------------------------------------------------------------- /mindflow/extensions/extensions.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Run-Tu/MindFlow/3d3fdee8833236f3b1b459061b33bb13b59f5ee6/mindflow/extensions/extensions.txt -------------------------------------------------------------------------------- /mindflow/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 | -------------------------------------------------------------------------------- /mindflow/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 | -------------------------------------------------------------------------------- /mindflow/memory/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Run-Tu/MindFlow/3d3fdee8833236f3b1b459061b33bb13b59f5ee6/mindflow/memory/__init__.py -------------------------------------------------------------------------------- /mindflow/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 | -------------------------------------------------------------------------------- /mindflow/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 | -------------------------------------------------------------------------------- /mindflow/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 mindflow. [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 mindflow 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 `mindflow.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 `mindflow.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 | -------------------------------------------------------------------------------- /mindflow/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 35 | -------------------------------------------------------------------------------- /mindflow/profile/template.py: -------------------------------------------------------------------------------- 1 | from mindflow.profile import Profile 2 | from mindflow.utils import USERNAME, ROOT_DIR 3 | from mindflow.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 | ) 45 | -------------------------------------------------------------------------------- /mindflow/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 | -------------------------------------------------------------------------------- /mindflow/speech/stt.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Run-Tu/MindFlow/3d3fdee8833236f3b1b459061b33bb13b59f5ee6/mindflow/speech/stt.py -------------------------------------------------------------------------------- /mindflow/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 57 | -------------------------------------------------------------------------------- /mindflow/tests/__init__.py: -------------------------------------------------------------------------------- 1 | from ..core import MindFlow 2 | from rich import print 3 | import asyncio 4 | import os 5 | 6 | os.environ["API_KEY"] = "e8e85d70-74cd-43f7-bd5e-fd8dec181037" 7 | macro = MindFlow() 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() 33 | -------------------------------------------------------------------------------- /mindflow/tests/cases/browser.py: -------------------------------------------------------------------------------- 1 | def test(): 2 | from ...mindflow 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() 9 | -------------------------------------------------------------------------------- /mindflow/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"mindflow.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 284 | -------------------------------------------------------------------------------- /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 = "mindflow" 7 | version = "0.2.11" 8 | description = "Multimodal Assistant. Human Interface for computers." 9 | authors = ["Run Tu "] 10 | license = "MIT" 11 | readme = "README.md" 12 | homepage = "https://github.com/run-tu/mindflow" 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 = "mindflow.__main__:main" 28 | omi = "mindflow.omi:main" 29 | 30 | [tool.poetry.extras] 31 | dev = ["pytest"] 32 | --------------------------------------------------------------------------------