Skip to content
Merged
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
1 change: 1 addition & 0 deletions Autotests/run_mandatory
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ test_openai_runtime_embeddings.py
test_prompt_grounding.py
test_websearch_smoke.py
test_wschat.py
unit/test_config.py
unit/test_fileio_verified_writes.py
unit/test_fileio_verified_deletes.py
unit/test_helper_parsing.py
Expand Down
103 changes: 103 additions & 0 deletions Autotests/unit/test_config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import pytest

import config
from config import command_line_to_dict


def test_command_line_numeric_values_match_yaml_scalar_types(isolated_config):
config._COMMAND_LINE = command_line_to_dict(
["maxFeedback=25000", "sleepInterval=0.5", "temperature=-1.25e-2"]
)
expected = {
"maxFeedback": 25000,
"sleepInterval": 0.5,
"temperature": -0.0125,
}
config._CONFIG_FILE = expected.copy()
for key, value in expected.items():
actual = config.config_get_by_key(key)
assert actual == value
assert type(actual) is type(value)


def test_command_line_strings_and_flag_keep_existing_semantics():
values = command_line_to_dict(
["provider=OpenAI", "IRC_channel=001", "maxFeedback"]
)

assert values == {
"provider": "OpenAI",
"IRC_channel": "001",
"maxFeedback": True,
}


@pytest.fixture
def isolated_config(monkeypatch):
for name in ("_CONFIG", "_COMMAND_LINE", "_CONFIG_FILE"):
monkeypatch.setattr(config, name, {})


@pytest.mark.parametrize("default", [50000, 0, 0.5])
def test_bare_numeric_option_rejected_from_default(isolated_config, default):
config._COMMAND_LINE = command_line_to_dict(["maxFeedback"])
with pytest.raises(ValueError, match="maxFeedback.*numeric value"):
config.config_get_by_key("maxFeedback", default)


def test_bare_numeric_option_rejected_from_yaml(isolated_config):
config._COMMAND_LINE = command_line_to_dict(["maxFeedback"])
config._CONFIG_FILE = {"maxFeedback": 50000}
with pytest.raises(ValueError, match="maxFeedback.*numeric value"):
config.config_get_by_key("maxFeedback")


def test_cached_bare_option_cannot_bypass_numeric_validation(isolated_config):
config._COMMAND_LINE = command_line_to_dict(["maxFeedback"])
assert config.config_get_by_key("maxFeedback") is True
with pytest.raises(ValueError, match="maxFeedback.*numeric value"):
config.config_get_by_key("maxFeedback", 50000)


@pytest.mark.parametrize("default", [False, True, None])
def test_bare_boolean_flags_remain_supported(isolated_config, default):
config._COMMAND_LINE = command_line_to_dict(["memoryExportEnabled"])
config._CONFIG_FILE = {"memoryExportEnabled": False}
assert config.config_get_by_key("memoryExportEnabled", default) is True


def test_explicit_numeric_override_reaches_config_consumer(isolated_config):
config._COMMAND_LINE = command_line_to_dict(["maxFeedback=25000"])
config._CONFIG_FILE = {"maxFeedback": 50000}
value = config.config_get_by_key("maxFeedback", 50000)
assert type(value) is int
assert value + 1 == 25001


@pytest.mark.parametrize("key,value,default", [
("IRC_channel", "123", "##omega"),
("IRC_channel", "001", "##omega"),
("OPENROUTER_SESSION_ID", "123", None),
("WS_TOKEN", "1e3", ""),
("model", "0.5", "default-model"),
("config", "123", "config/config.yaml"),
])
def test_numeric_looking_string_settings_are_preserved(
isolated_config, key, value, default
):
config._COMMAND_LINE = command_line_to_dict([f"{key}={value}"])
actual = config.config_get_by_key(key, default)
assert actual == value
assert type(actual) is str


def test_cached_untyped_lookup_does_not_prevent_numeric_conversion(isolated_config):
config._COMMAND_LINE = command_line_to_dict(["maxFeedback=25000"])
assert config.config_get_by_key("maxFeedback") == "25000"
assert config.config_get_by_key("maxFeedback", 50000) == 25000


def test_string_default_takes_precedence_over_numeric_yaml(isolated_config):
config._COMMAND_LINE = command_line_to_dict(["IRC_channel=123"])
config._CONFIG_FILE = {"IRC_channel": 456}
assert config.config_get_by_key("IRC_channel", "##omega") == "123"
16 changes: 13 additions & 3 deletions docs/reference-configuration.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
# Reference — Configuration

Every tunable in Omega is declared as `(= (name) (empty))` and later bound by a `configure` call inside an `init*` function. The `configure` helper in `src/utils.metta` is:
Every tunable in Omega is declared as `(= (name) (empty))` and later bound by a `configure` call inside an `init*` function. The `configure` helper in `src/config.metta` is:

```metta
(= (configure $name $default)
(let $value (argk $name $default)
(let $value (configGetByKey $name $default)
(add-atom &self (= ($name) $value))))
```

This reads a command-line override via `argk` (`name=value` on the MeTTa command line) if present, otherwise falls back to the default.
This resolves the value through the Python configuration layer, using command-line overrides, environment variables, YAML configuration, and the caller's default in that order.

## Loop (`src/loop.metta`, `initLoop`)

Expand Down Expand Up @@ -75,6 +75,16 @@ Configuration values are resolved in this order: command-line `key=value`,
default. `TG_BOT_TOKEN` and `OMEGA_AUTH_SECRET` are read directly from the
environment and must be placed before the `metta`/`petta` command.

For numeric settings, unambiguous integer and floating-point command-line values
are parsed as numbers: `maxFeedback=25000` and `sleepInterval=0.5` can be used in
arithmetic. The caller's default determines the setting's type; when no default is
provided, the YAML value is used. String settings and settings without a known
type keep their CLI text unchanged, including `IRC_channel=123`,
`IRC_channel=001`, and `OPENROUTER_SESSION_ID=123`.
A bare argument remains a boolean flag, but a numeric setting requires
`key=<number>`. For example,
`maxFeedback` without a value raises an error naming the parameter when resolved.

Telegram example:

```bash
Expand Down
30 changes: 28 additions & 2 deletions src/config.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import os
import logging
import pathlib
import re
import yaml

logger = logging.getLogger(__name__)
Expand All @@ -10,6 +11,11 @@
_COMMAND_LINE = {}
_CONFIG_FILE = {}

_INTEGER_LITERAL = re.compile(r"^[+-]?(?:0|[1-9][0-9]*)$")
_FLOAT_LITERAL = re.compile(
r"^[+-]?(?!0[0-9])(?:(?:[0-9]+\.[0-9]*|\.[0-9]+)(?:[eE][+-]?[0-9]+)?|[0-9]+[eE][+-]?[0-9]+)$"
)

def init_config(command_line):
"""Initialize configuration: drop configuration cache and load
configuration file"""
Expand All @@ -33,10 +39,21 @@ def config_get_by_key(key, default=None):
parameters, (2) environment variable with OMEGA_$key name, (3)
configuration file, (4) use $default value."""
global _CONFIG, _COMMAND_LINE, _CONFIG_FILE
if key in _COMMAND_LINE:
value = _COMMAND_LINE[key]
expected = default if default is not None else _CONFIG_FILE.get(key)
if type(expected) in (int, float):
if value is True:
raise ValueError(
f"{key} requires a numeric value; use {key}=<number>"
)
value = _command_line_value(value)
# A later caller can supply a type that an earlier lookup did not know.
if key in _CONFIG and type(_CONFIG[key]) is type(value) and _CONFIG[key] == value:
return _CONFIG[key]
return _cache_config(key, value, "command line")
if key in _CONFIG:
return _CONFIG.get(key)
if key in _COMMAND_LINE:
return _cache_config(key, _COMMAND_LINE.get(key), "command line")
envkey = f"OMEGA_{key}"
if envkey in os.environ:
return _cache_config(key, os.environ.get(envkey), "environment variable")
Expand All @@ -50,6 +67,15 @@ def _cache_config(key, value, source):
logger.info(f"Configuration item resolved using {source}: {key}={value}")
return value

def _command_line_value(value):
"""Parse a CLI value for a setting known to be numeric."""
if _INTEGER_LITERAL.fullmatch(value):
return int(value)
if _FLOAT_LITERAL.fullmatch(value):
return float(value)
return value


def command_line_to_dict(list):
"""Converts list of <key>=<value> pairs into Python dictionary. If
parameter doesn't include "=" it is added as a boolean value
Expand Down
12 changes: 12 additions & 0 deletions tests/src_config.metta
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,18 @@
!(test (configGetByKey arg_env val_default) val_env)
!(test (configGetByKey arg_notexist val_default) val_default)

; Numeric CLI values must survive configure and Prolog arithmetic.
!(cleanup)
(= (command-line)
("config=./tests/res/src_config_empty.yaml"
"maxFeedback=25000"
"sleepInterval=0.5"))
!(initConfig)
!(configure maxFeedback 50000)
!(configure sleepInterval 1)
!(test (+ (maxFeedback) 1) 25001)
!(test (+ (sleepInterval) 0.5) 1.0)

!(cleanup)

; filled configuration
Expand Down
Loading