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
28 changes: 23 additions & 5 deletions Autotests/import_knowledge/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ The integration tests start and remove the container themselves through `scripts

## Unit: scripts/import_knowledge.sh

The script reads `EMBEDDING_PROVIDER`, runs `import-knowledge` (or `import-knowledge --local`), and writes a per-provider sentinel under `CHROMA_DB_PATH` so a second start skips the import. The stub on PATH records how `import-knowledge` was called.
The script reads `EMBEDDING_PROVIDER` and `EMBEDDING_MODEL`, runs `import-knowledge --provider openai|asicloud [--model <model>]` (or `import-knowledge --local`), and writes a per-provider sentinel under `CHROMA_DB_PATH` so a second start skips the import. The stub on PATH records how `import-knowledge` was called.

### 1. test_local_runs_import_and_writes_sentinel

Expand All @@ -49,7 +49,7 @@ Local provider runs the import and leaves the local sentinel behind.

OpenAI with `OPENAI_API_KEY` set runs the import and leaves the OpenAI sentinel.

- Checks: exit 0; the stub was called without `--local`; `.import-kb.openai.done` exists.
- Checks: exit 0; the stub was called with `--provider openai`; `.import-kb.openai.done` exists.

### 3. test_openai_without_key_exit1

Expand Down Expand Up @@ -99,23 +99,41 @@ When `import-knowledge` exits non-zero, no sentinel is written, so the next star

- Checks: non-zero exit; the stub was called; `.import-kb.local.done` is absent.

### 11. test_asicloud_with_key_runs_import_and_writes_sentinel

ASICloud with `ASI_API_KEY` set runs the import and leaves the ASICloud sentinel.

- Checks: exit 0; the stub was called with `--provider asicloud`; `.import-kb.asicloud.done` exists.

### 12. test_asicloud_without_key_exit1

ASICloud without a key stops before importing.

- Checks: exit 1; stderr says `ASI_API_KEY is required`; the stub was never called.

### 13. test_embedding_model_is_passed_to_the_import

`EMBEDDING_MODEL` reaches the import as `--model`.

- Checks: exit 0; the stub was called with `--provider asicloud --model BAAI/bge-base-en-v1.5`.

## Integration: container startup

They launch the image through `scripts/omega`, so the real entrypoint runs (nginx, env scrub, import gating).

### 11. test_entrypoint_imports_when_enabled
### 14. test_entrypoint_imports_when_enabled

With `IMPORT_KB_ON_START=1` the entrypoint starts the import on boot.

- Checks: `[import-kb] Running` appears in the container log within 180 s.

### 12. test_entrypoint_skips_when_disabled
### 15. test_entrypoint_skips_when_disabled

With `IMPORT_KB_ON_START=0` the entrypoint never touches import-kb.

- Checks: after a 25 s window no `[import-kb]` line appears in the log.

### 13. test_local_real_import_runs
### 16. test_local_real_import_runs

A real local import runs and lands in the same `chroma_db` the agent reads from.

Expand Down
32 changes: 31 additions & 1 deletion Autotests/import_knowledge/test_import_knowledge.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,9 @@ def env(tmp_path):
e["CHROMA_DB_PATH"] = str(chroma)
e.pop("IMPORT_KB_FORCE", None)
e.pop("OPENAI_API_KEY", None)
e.pop("ASI_API_KEY", None)
e.pop("EMBEDDING_PROVIDER", None)
e.pop("EMBEDDING_MODEL", None)
return {"env": e, "marker": marker, "chroma": chroma}


Expand Down Expand Up @@ -62,7 +64,7 @@ def test_openai_with_key_runs_import_and_writes_sentinel(env):
r = run(env["env"])
assert r.returncode == 0, r.stderr
assert env["marker"].exists()
assert env["marker"].read_text().strip() == ""
assert env["marker"].read_text().strip() == "--provider openai"
assert (env["chroma"] / ".import-kb.openai.done").exists()


Expand Down Expand Up @@ -165,3 +167,31 @@ def test_failed_import_does_not_write_sentinel(env):
assert r.returncode != 0
assert env["marker"].exists()
assert not (env["chroma"] / ".import-kb.local.done").exists()


def test_asicloud_with_key_runs_import_and_writes_sentinel(env):
env["env"]["EMBEDDING_PROVIDER"] = "ASICloud"
env["env"]["ASI_API_KEY"] = "dummy-key"
r = run(env["env"])
assert r.returncode == 0, r.stderr
assert env["marker"].read_text().strip() == "--provider asicloud"
assert (env["chroma"] / ".import-kb.asicloud.done").exists()


def test_asicloud_without_key_exit1(env):
env["env"]["EMBEDDING_PROVIDER"] = "ASICloud"
r = run(env["env"])
assert r.returncode == 1
assert "ASI_API_KEY is required" in r.stderr
assert not env["marker"].exists()


def test_embedding_model_is_passed_to_the_import(env):
env["env"]["EMBEDDING_PROVIDER"] = "ASICloud"
env["env"]["ASI_API_KEY"] = "dummy-key"
env["env"]["EMBEDDING_MODEL"] = "BAAI/bge-base-en-v1.5"
r = run(env["env"])
assert r.returncode == 0, r.stderr
assert env["marker"].read_text().strip() == (
"--provider asicloud --model BAAI/bge-base-en-v1.5"
)
1 change: 1 addition & 0 deletions Autotests/run_mandatory
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,4 @@ unit/test_fileio_verified_writes.py
unit/test_fileio_verified_deletes.py
unit/test_helper_parsing.py
unit/test_openclaw_unit.py
import_knowledge/test_import_knowledge.py
66 changes: 61 additions & 5 deletions Autotests/test_openai_runtime_embeddings.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,24 @@
import types
from pathlib import Path

import pytest


REPO_ROOT = Path(__file__).resolve().parents[1]
RAG_MODULE_PATH = REPO_ROOT / "src" / "rag.py"
MEMORY_METTA_PATH = REPO_ROOT / "src" / "memory.metta"


def load_rag_module(monkeypatch):
def load_rag_module(monkeypatch, config=None, expected_model="text-embedding-3-large",
error=None):
created_clients = []
settings = {"GATEWAY_URL": "http://gateway:8080", **(config or {})}

class FakeEmbeddings:
def create(self, *, model, input):
assert model == "text-embedding-3-large"
if error is not None:
raise error
assert model == expected_model
assert input == ["runtime probe"]
return types.SimpleNamespace(
data=[types.SimpleNamespace(embedding=[0.1, 0.2, 0.3])]
Expand All @@ -32,7 +38,7 @@ def __init__(self, *, base_url=None, api_key=None):
chromadb_module = types.ModuleType("chromadb")
config_module = types.ModuleType("config")
config_module.config_get_by_key = (
lambda key, default=None: "http://gateway:8080" if key == "GATEWAY_URL" else default
lambda key, default=None: settings.get(key, default)
)
llm_module = types.ModuleType("lib_llm_ext")
llm_module.initLocalEmbedding = lambda: None
Expand All @@ -53,14 +59,64 @@ def __init__(self, *, base_url=None, api_key=None):
def test_runtime_openai_embedding_uses_proxy_and_returns_single_vector(monkeypatch):
rag, clients = load_rag_module(monkeypatch)

assert rag.openai_embed("runtime probe") == [0.1, 0.2, 0.3]
assert rag.cloud_embed("runtime probe") == [0.1, 0.2, 0.3]
assert len(clients) == 1
assert clients[0].base_url == "http://gateway:8080/openai/"
assert clients[0].api_key == "unused"


def test_runtime_embedding_uses_the_configured_provider_and_model(monkeypatch):
rag, clients = load_rag_module(
monkeypatch,
config={"embeddingprovider": "ASICloud",
"embeddingModel": "WhereIsAI/UAE-Large-V1"},
expected_model="WhereIsAI/UAE-Large-V1",
)

assert rag.cloud_embed("runtime probe") == [0.1, 0.2, 0.3]
assert clients[0].base_url == "http://gateway:8080/asicloud/"


def test_runtime_asicloud_without_model_uses_the_asicloud_default(monkeypatch):
rag, clients = load_rag_module(
monkeypatch,
config={"embeddingprovider": "ASICloud"},
expected_model="WhereIsAI/UAE-Large-V1",
)

assert rag.cloud_embed("runtime probe") == [0.1, 0.2, 0.3]
assert clients[0].base_url == "http://gateway:8080/asicloud/"


def test_runtime_empty_model_falls_back_to_the_provider_default(monkeypatch):
rag, _ = load_rag_module(
monkeypatch,
config={"embeddingprovider": "ASICloud", "embeddingModel": ""},
expected_model="WhereIsAI/UAE-Large-V1",
)

assert rag.cloud_embed("runtime probe") == [0.1, 0.2, 0.3]


def test_runtime_embedding_failure_logs_the_provider_error(monkeypatch):
rag, _ = load_rag_module(
monkeypatch,
config={"embeddingprovider": "ASICloud"},
error=Exception("Error code: 400 - {'error': 'Model not found'}"),
)
logged = []
rag.logger = types.SimpleNamespace(
error=lambda message, *args, **kwargs: logged.append(message)
)

with pytest.raises(RuntimeError):
rag.cloud_embed("runtime probe")

assert any("Model not found" in message for message in logged)


def test_memory_metta_routes_openai_embeddings_to_rag_wrapper():
memory_metta = MEMORY_METTA_PATH.read_text(encoding="utf-8")

assert "(py-call (rag.openai_embed (string-safe $str)))" in memory_metta
assert "(py-call (rag.cloud_embed (string-safe $str)))" in memory_metta
assert "useGPTEmbedding" not in memory_metta
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ After start go to https://webchat.quakenet.org/ to communicate with the agent. J

If you are running Omega without Docker and would like to load it with preset knowledge, follow these steps:

1. Set EMBEDDING_PROVIDER in your environment. It can be set to either OpenAI or Local. OpenAI embeddings also require OPENAI_API_KEY to be set in your environment.
1. Set EMBEDDING_PROVIDER in your environment. It can be set to OpenAI, ASICloud or Local. OpenAI embeddings also require OPENAI_API_KEY, and ASICloud embeddings require ASI_API_KEY to be set in your environment.

2. Run:
```
Expand Down
5 changes: 4 additions & 1 deletion config/config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,11 @@ maxEpisodeRecallLines: 20
maxHistory: 30000
# ChromaDB persistence directory used for long-term memory portability.
chromaDbPath: "./chroma_db"
# `Local` (Python-side model) or `OpenAI` (requires `OPENAI_API_KEY`)
# `Local` (Python-side model) or the id of a provider serving OpenAI-compatible embeddings: `OpenAI`, `ASICloud`
embeddingprovider: Local
# Model asked of a non-`Local` embeddingprovider; empty means the provider default:
# `text-embedding-3-large` for `OpenAI`, `WhereIsAI/UAE-Large-V1` for `ASICloud`
embeddingModel: ""
# Enable authenticated operator-triggered /memory-export commands (disabled by default).
memoryExportEnabled: false

Expand Down
3 changes: 2 additions & 1 deletion docs/reference-configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@ This reads a command-line override via `argk` (`name=value` on the MeTTa command
| `maxHistory` | 30000 (chars) | Tail of `memory/history.metta` included in the prompt. |
| `memoryDirectory` | `./repos/Omega/memory` | Directory containing persistent memory files such as `history.metta`. |
| `chromaDbPath` | `./chroma_db` | ChromaDB persistence directory used for memory backup and restore. |
| `embeddingprovider` | `Local` | `Local` (Python-side model) or `OpenAI`. |
| `embeddingprovider` | `Local` | `Local` (Python-side model), or the id of a provider that serves an OpenAI-compatible `/embeddings` endpoint — `OpenAI` and `ASICloud` are known to. The gateway supplies that provider's key. |
| `embeddingModel` | empty | Model asked of a non-`Local` `embeddingprovider`. Empty means the provider default: `text-embedding-3-large` for `OpenAI`, `WhereIsAI/UAE-Large-V1` for `ASICloud`. |

## Channels (`src/channels.metta`, `initChannels`)

Expand Down
8 changes: 6 additions & 2 deletions docs/reference-internals-extension-points.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,10 +53,14 @@ In `src/memory.metta`, the `embed` function dispatches on `embeddingprovider`:
(= (embed $str)
(if (== (embeddingprovider) Local)
(py-call (lib_llm_ext.useLocalEmbedding (string-safe $str)))
(useGPTEmbedding (string-safe $str))))
(py-call (rag.cloud_embed (string-safe $str)))))
```

To add a new backend, add a branch and implement the Python function.
Any value other than `Local` is a provider id: the remote branch posts
`embeddingModel` to `<GATEWAY_URL>/<embeddingprovider lowercased>/`, the
location that already injects that provider's key. Switching vendor is
configuration, not code — provided the vendor serves embeddings at all. It
changes the vector space, so reset the ChromaDB store when you do.

## Change the reasoning library

Expand Down
2 changes: 1 addition & 1 deletion docs/reference-skills-memory.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ The result of the ChromaDB write (internally). The agent treats a successful cal

### Notes / Limits
- Text is passed through `string-safe` before embedding, which escapes newlines, quotes, and apostrophes.
- Embedding provider is selected by `embeddingprovider` (`Local` or `OpenAI`).
- Embedding provider is selected by `embeddingprovider`, the model by `embeddingModel`.
- Nothing deduplicates automatically — repeated `remember` calls store multiple items.

---
Expand Down
2 changes: 1 addition & 1 deletion docs/tutorial-01-teaching-memories.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ From `src/memory.metta`:
- `maxRecallItems` — how many items `query` returns (default 20).
- `maxEpisodeRecallLines` — how many lines `episodes` returns (default 20).
- `maxHistory` — characters of history fed back into the prompt (default 30000).
- `embeddingprovider` — `OpenAI` or `Local`.
- `embeddingprovider` — `OpenAI`, `ASICloud` or `Local`.

Change any of these by editing the `configure` calls in `initMemory` or passing command-line overrides — see [reference-configuration.md](./reference-configuration.md).

Expand Down
13 changes: 9 additions & 4 deletions entrypoint.sh
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,17 @@ nginx_url() {
cd /PeTTa

EMBEDDING_PROVIDER="${EMBEDDING_PROVIDER:-Local}"
EMBEDDING_MODEL="${EMBEDDING_MODEL:-}"
OPENAIAPI_URL="http://localhost:8080/" # dummy value
MM_URL="http://localhost:8080/" # dummy value
OPENCLAW_URL="http://localhost:8080/" # dummy value
for arg in "$@"; do
if [[ "$arg" == embeddingprovider=* ]]; then
EMBEDDING_PROVIDER="${arg#*=}"
fi
if [[ "$arg" == embeddingModel=* ]]; then
EMBEDDING_MODEL="${arg#*=}"
fi
# URL to redirect OpenAIAPI provider requests
if [[ "$arg" == openaiapi_url=* ]]; then
OPENAIAPI_URL=$(nginx_url "${arg#*=}")
Expand All @@ -31,7 +35,7 @@ for arg in "$@"; do
OPENCLAW_URL=$(nginx_url "${arg#*=}")
fi
done
export EMBEDDING_PROVIDER OPENAIAPI_URL MM_URL OPENCLAW_URL
export EMBEDDING_PROVIDER EMBEDDING_MODEL OPENAIAPI_URL MM_URL OPENCLAW_URL

su www-data -s /bin/sh -c "sh /opt/nginx/nginx.sh"

Expand All @@ -41,11 +45,12 @@ if [[ "${IMPORT_KB_ON_START}" == "1" ]]; then
fi

MEMORY_PORTABILITY_PYTHON='import os
import sys
from config import init_config
from memory_export import create_memory_store
from memory_portability import MemoryTransfer

init_config([])
init_config(sys.argv[1:])
transfer = MemoryTransfer(
transfer_dir="/memory-transfer",
store=create_memory_store(),
Expand All @@ -66,13 +71,13 @@ export MEMORY_PORTABILITY_PYTHON
export PYTHONPATH="${OMEGA_DIR}:${OMEGA_DIR}/src${PYTHONPATH:+:${PYTHONPATH}}"

export MEMORY_PORTABILITY_OPERATION=recover
su nobody -s /bin/sh -c 'exec python3 -c "$MEMORY_PORTABILITY_PYTHON"' \
su nobody -s /bin/sh -c 'exec python3 -c "$MEMORY_PORTABILITY_PYTHON" "$@"' sh "$@" \
|| { echo "Memory import recovery failed. Aborting startup." >&2; exit 1; }

if [[ -n "${MEMORY_IMPORT_FILE:-}" ]]; then
echo "memory_portability: importing ${MEMORY_IMPORT_FILE}"
export MEMORY_PORTABILITY_OPERATION=import
su nobody -s /bin/sh -c 'exec python3 -c "$MEMORY_PORTABILITY_PYTHON"' \
su nobody -s /bin/sh -c 'exec python3 -c "$MEMORY_PORTABILITY_PYTHON" "$@"' sh "$@" \
|| { echo "Memory import failed. Aborting startup." >&2; exit 1; }
echo "memory_portability: import complete"
fi
Expand Down
2 changes: 1 addition & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ chromadb==1.5.9
openai==2.38.0
transformers==5.8.0
sentence-transformers==5.5.1
import-kb==0.2.3
import-kb==0.2.4
py-landlock==0.1.1
pyyaml==6.0.3
ddgs==9.14.4
Expand Down
Loading
Loading