Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Autotests/unit/test_llm_budget.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ def _load_modules():
try:
loaded = {"providers": _exec("providers", os.path.join(_SRC_DIR, "providers.py"), True)}
for name in ("lib_llm_ext", "openrouter", "openai_provider", "asione"):
file_name = "openai.py" if name == "openai_provider" else f"{name}.py"
file_name = "omega_openai.py" if name == "openai_provider" else f"{name}.py"
loaded[name] = _exec(name, os.path.join(_PROVIDERS_DIR, file_name), name == "lib_llm_ext")
return loaded
finally:
Expand Down
2 changes: 1 addition & 1 deletion config/plugins.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@
loader: python
location: "{REPO}/providers"

- name: openai
- name: omega_openai
loader: python
location: "{REPO}/providers"

Expand Down
33 changes: 33 additions & 0 deletions docs/reference-plugin-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,39 @@ module. The plugin record has the following fields:
`name` module is located. Can include `{REPO}` placeholder to designate the
root folder of the Omega source repository.

### Python module identity and names

Python plugins are registered in `sys.modules` under their configured `name`,
including when loaded from a `location`. After loading, `import <name>` returns
the same module object used by Omega, sharing its initialized state and queues.
The module is registered before its code executes, so imports made during
initialization can find it. As with ordinary Python imports, code must not use
attributes that have not been initialized yet.

If the same file was already imported under that name, Omega reuses the module
and calls `loadOmegaPlugin`. Loading an already registered plugin again does
not execute its code or call its entry point again. This is not a hot-reload API.
For location-based loading, a name occupied by a different module, or by a
module without a file path, causes an error instead of replacing that module.
Choose plugin names that do not conflict with dependencies or standard-library
modules, even if those modules have not been imported yet. The loader checks
the existing import path before adding the plugin directory and rejects a
name that resolves to another module or namespace package. A name that
resolves to the same plugin file is allowed.

If module execution fails, its newly added `sys.modules` entry is removed and
the original exception is propagated. A missing or failing `loadOmegaPlugin`
does not leave a successful entry in Omega's plugin registry. Once module
execution succeeds, the module stays imported even if its entry point fails;
callbacks or other side effects already performed by that entry point are not
automatically undone.

The built-in OpenAI plugin file is now `providers/omega_openai.py`. If you keep
a custom `plugins.yaml`, change its Python plugin entry from `name: openai` to
`name: omega_openai`, retaining `location: "{REPO}/providers"`. The provider
identifier remains `OpenAI`, so `provider=OpenAI` and API key configuration do
not change. `import openai` continues to refer to the OpenAI SDK.

As an example of a MeTTa plugin one can look at the code of the [workflow
plugin](/plugins/workflow/workflow.metta). As an example of a Python plugin
one can look at the code of the [IRC communication channel](/channels/irc.py).
Expand Down
File renamed without changes.
54 changes: 42 additions & 12 deletions src/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,30 +77,60 @@ def loadPythonPlugin(name, location):
specified it imports "<location>/<name>.py" file. Imports <name> Python
module otherwise. Calls "loadOmegaPlugin" function from the imported
module. This is the point where plugin's code gets control and should
register appropriate callbacks."""
register appropriate callbacks. Repeated loads of the same module do not
call the entry point again."""
global _plugins, _REPO

mod = None
if not location:
logger.info(f"_initPythonPlugin: loading {name} plugin from PYTHONPATH using Python module loader")
mod = importlib.import_module(name)
else:
# adding location into sys.path to be able loading plugins which
# consist of multiple files
addLocationToPath(location)
location = pathlib.Path(location.format(REPO=_REPO)).resolve()
modpath = location.joinpath(f"{name}.py").resolve()
logger.info(f"_initPythonPlugin: loading {name} plugin from {modpath} using Python module loader")
spec = importlib.util.spec_from_file_location(name, modpath)
mod = importlib.util.module_from_spec(spec)
spec.loader.exec_module(mod)

if mod is not None:
_plugins[name] = PythonPlugin(mod)
else:
if name in sys.modules:
mod = sys.modules[name]
existing_path = getattr(mod, "__file__", None)
if existing_path is None or pathlib.Path(existing_path).resolve() != modpath:
_error("_initPythonPlugin", f"Plugin name '{name}' is already used by another module ({existing_path}); cannot load {modpath}")
else:
# Also protect dependencies that are importable but not loaded yet.
existing_spec = importlib.util.find_spec(name)
if existing_spec is not None:
existing_path = existing_spec.origin
if existing_path is None or existing_path in ("built-in", "frozen") or pathlib.Path(existing_path).resolve() != modpath:
_error("_initPythonPlugin", f"Plugin name '{name}' is already used by another importable module ({existing_path}); cannot load {modpath}; choose a different plugin name")

# Multi-file plugins need their folder available for sibling imports.
if str(location) not in sys.path:
addLocationToPath(str(location))
if mod is None:
spec = importlib.util.spec_from_file_location(name, modpath)
mod = importlib.util.module_from_spec(spec)
# Match normal import semantics, including imports during execution.
sys.modules[name] = mod
try:
spec.loader.exec_module(mod)
except BaseException:
if sys.modules.get(name) is mod:
del sys.modules[name]
raise

if mod is None:
_error("_initPythonPlugin", f"Couldn't find Python module {name}")

if name in _plugins:
if _plugins[name].mod is mod:
return
_error("_initPythonPlugin", f"Plugin name '{name}' is already registered with another module")

if not hasattr(mod, "loadOmegaPlugin"):
_error("_initPythonPlugin", f"No loadOmegaPlugin() function is implemented by plugin {name}")
plugin_loader = getattr(mod, "loadOmegaPlugin")
plugin_loader()
_plugins[name] = PythonPlugin(mod)
try:
plugin_loader()
except BaseException:
del _plugins[name]
raise
Loading
Loading