├── .gitattributes
├── .github
└── workflows
│ └── pylint.yml
├── .gitignore
├── LICENSE
├── README.md
├── docs
├── images
│ ├── demo.png
│ └── openmacro-title.svg
└── videos
│ └── demo.mov
├── openmacro
├── __init__.py
├── __main__.py
├── cli.py
├── computer
│ └── __init__.py
├── core
│ ├── __init__.py
│ └── prompts
│ │ ├── conversational.txt
│ │ ├── initial.txt
│ │ ├── instructions.txt
│ │ └── memorise.txt
├── extensions
│ ├── __init__.py
│ ├── browser
│ │ ├── __init__.py
│ │ ├── config.default.toml
│ │ ├── docs
│ │ │ └── instructions.md
│ │ ├── src
│ │ │ ├── engines.json
│ │ │ └── user_agents.txt
│ │ ├── tests
│ │ │ └── tests.py
│ │ └── utils
│ │ │ ├── general.py
│ │ │ └── google.py
│ ├── email
│ │ ├── __init__.py
│ │ └── docs
│ │ │ └── instructions.md
│ └── extensions.txt
├── llm
│ ├── __init__.py
│ └── models
│ │ └── samba.py
├── memory
│ ├── __init__.py
│ ├── client.py
│ └── server.py
├── omi
│ └── __init__.py
├── profile
│ ├── __init__.py
│ └── template.py
├── speech
│ ├── __init__.py
│ ├── stt.py
│ └── tts.py
├── tests
│ ├── __init__.py
│ └── cases
│ │ └── browser.py
└── utils
│ └── __init__.py
├── poetry.lock
└── pyproject.toml
/.gitattributes:
--------------------------------------------------------------------------------
1 | # Auto detect text files and perform LF normalization
2 | * text=auto
3 |
--------------------------------------------------------------------------------
/.github/workflows/pylint.yml:
--------------------------------------------------------------------------------
1 | name: Pylint
2 |
3 | on: [push]
4 |
5 | jobs:
6 | build:
7 | runs-on: ubuntu-latest
8 | strategy:
9 | matrix:
10 | python-version: ["3.8", "3.9", "3.10"]
11 | steps:
12 | - uses: actions/checkout@v4
13 | - name: Set up Python ${{ matrix.python-version }}
14 | uses: actions/setup-python@v3
15 | with:
16 | python-version: ${{ matrix.python-version }}
17 | - name: Install dependencies
18 | run: |
19 | python -m pip install --upgrade pip
20 | pip install pylint
21 | - name: Analysing the code with pylint
22 | run: |
23 | pylint $(git ls-files '*.py')
24 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Byte-compiled / optimized / DLL files
2 | __pycache__/
3 | *.py[cod]
4 | *$py.class
5 |
6 | # C extensions
7 | *.so
8 |
9 | # Distribution / packaging
10 | config.toml
11 | .Python
12 | build/
13 | develop-eggs/
14 | dist/
15 | downloads/
16 | eggs/
17 | .eggs/
18 | lib/
19 | lib64/
20 | parts/
21 | sdist/
22 | var/
23 | wheels/
24 | share/python-wheels/
25 | *.egg-info/
26 | .installed.cfg
27 | *.egg
28 | MANIFEST
29 |
30 | # PyInstaller
31 | # Usually these files are written by a python script from a template
32 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
33 | *.manifest
34 | *.spec
35 |
36 | # Installer logs
37 | pip-log.txt
38 | pip-delete-this-directory.txt
39 |
40 | # Unit test / coverage reports
41 | htmlcov/
42 | .tox/
43 | .nox/
44 | .coverage
45 | .coverage.*
46 | .cache
47 | nosetests.xml
48 | coverage.xml
49 | *.cover
50 | *.py,cover
51 | .hypothesis/
52 | .pytest_cache/
53 | cover/
54 |
55 | # Translations
56 | *.mo
57 | *.pot
58 |
59 | # Django stuff:
60 | *.log
61 | local_settings.py
62 | db.sqlite3
63 | db.sqlite3-journal
64 |
65 | # ChromaDB stuff:
66 | chroma.sqlite3
67 |
68 | # Openmacro stuff:
69 | profiles/
70 |
71 | # Flask stuff:
72 | instance/
73 | .webassets-cache
74 |
75 | # Scrapy stuff:
76 | .scrapy
77 |
78 | # Sphinx documentation
79 | docs/_build/
80 |
81 | # PyBuilder
82 | .pybuilder/
83 | target/
84 |
85 | # Jupyter Notebook
86 | .ipynb_checkpoints
87 |
88 | # IPython
89 | profile_default/
90 | ipython_config.py
91 |
92 | # pyenv
93 | # For a library or package, you might want to ignore these files since the code is
94 | # intended to run in multiple environments; otherwise, check them in:
95 | # .python-version
96 |
97 | # pipenv
98 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
99 | # However, in case of collaboration, if having platform-specific dependencies or dependencies
100 | # having no cross-platform support, pipenv may install dependencies that don't work, or not
101 | # install all needed dependencies.
102 | #Pipfile.lock
103 |
104 | # poetry
105 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
106 | # This is especially recommended for binary packages to ensure reproducibility, and is more
107 | # commonly ignored for libraries.
108 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
109 | #poetry.lock
110 |
111 | # pdm
112 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
113 | #pdm.lock
114 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
115 | # in version control.
116 | # https://pdm.fming.dev/#use-with-ide
117 | .pdm.toml
118 |
119 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
120 | __pypackages__/
121 |
122 | # Celery stuff
123 | celerybeat-schedule
124 | celerybeat.pid
125 |
126 | # SageMath parsed files
127 | *.sage.py
128 |
129 | # Environments
130 | .env
131 | .venv
132 | env/
133 | venv/
134 | ENV/
135 | env.bak/
136 | venv.bak/
137 |
138 | # Spyder project settings
139 | .spyderproject
140 | .spyproject
141 |
142 | # Rope project settings
143 | .ropeproject
144 |
145 | # mkdocs documentation
146 | /site
147 |
148 | # mypy
149 | .mypy_cache/
150 | .dmypy.json
151 | dmypy.json
152 |
153 | # Pyre type checker
154 | .pyre/
155 |
156 | # pytype static type analyzer
157 | .pytype/
158 |
159 | # Cython debug symbols
160 | cython_debug/
161 |
162 | # PyCharm
163 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
164 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
165 | # and can be added to the global gitignore or merged into this file. For a more nuclear
166 | # option (not recommended) you can uncomment the following to ignore the entire idea folder.
167 | #.idea/
168 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 amoooooo
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
6 |
7 |

8 |

9 |

10 |

11 |

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