├── .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 |
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 |
--------------------------------------------------------------------------------