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
15 changes: 15 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,21 @@ policy.

### Fixed

- **`OracleAgentMemoryManager` could not use OCI Generative AI models with a
profile or session token.** litellm's `oci/` driver, which Oracle's
`oracleagentmemory` client uses for its embedder and extraction model,
does not read `~/.oci/config`, so the manager's own defaults
(`oci/cohere.embed-v4.0`, `oci/openai.gpt-4.1`) failed with "Missing
required parameters" unless litellm's `OCI_USER` / `OCI_KEY_FILE`
variables were set. For `oci/...` specs the manager now builds the signer
from the OCI profile (`OCI_SESSION_PROFILE` / `OCI_PROFILE`, API key or
session token, instance and resource principals via `OCI_AUTH_TYPE`) and
passes it with `OCI_REGION` and `OCI_COMPARTMENT_ID`; `litellm_kwargs=`
overrides any of it and `oci_litellm_auth()` exposes the same derivation.
The manager also uses `MemoryExtractionConfig` where `oracleagentmemory`
provides it instead of the deprecated inline flag. Notebook 44 runs
OCI-only by default.

- **`OCIEmbeddings()` and `OracleAgentMemoryManager` failed with their
default embedding model.** OCI retired the Cohere v3.0 embedding models
from on-demand on 2026-09-30 (`404 Entity with key
Expand Down
4 changes: 3 additions & 1 deletion docs/concepts/memory-manager.md
Original file line number Diff line number Diff line change
Expand Up @@ -211,7 +211,9 @@ Two ways to keep long-term memory in Oracle Database 26ai. `OracleStore` is a
`BaseStore`, so it slots under `LLMMemoryManager` with the portable
extractor and per-user scoping; `OracleAgentMemoryManager` hands the whole
job to Oracle's own `oracleagentmemory` client (extraction, context cards,
retrieval scoped by user, agent and thread, all inside the database).
retrieval scoped by user, agent and thread, all inside the database). Both
default to OCI Generative AI models for embedding and extraction and take
the credentials from your OCI profile.

```python
import os
Expand Down
2 changes: 1 addition & 1 deletion examples/notebook_36_oci_tools.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Copyright (c) 2025, 2026 Oracle and/or its affiliates.
# Licensed under the Universal Permissive License v1.0 as shown at
# https://oss.oracle.com/licenses/upl/
"""Notebook 80: Drive the entire OCI control plane from natural language — `use_oci` + `describe_oci`.
"""Notebook 36: Drive the entire OCI control plane from natural language — `use_oci` + `describe_oci`.

Locus ships two built-in tools that, together, expose every OCI Python
SDK operation to an agent without per-service plumbing:
Expand Down
8 changes: 4 additions & 4 deletions examples/notebook_37_oracle_store.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

"""Notebook 37: a Locus agent with cross-thread memory on Oracle 26ai.

``oracle_checkpointer`` (notebook 03) persists *per-thread* agent
``oracle_checkpointer`` (notebook 42) persists *per-thread* agent
state — same ``thread_id`` resumes a conversation. :class:`OracleStore`
is the other half of long-term memory: a namespaced key/value store
for *cross-thread* facts. The Locus agent loop reaches into it through
Expand Down Expand Up @@ -43,7 +43,7 @@
export ORACLE_WALLET=~/.oci/wallets/mydb
export ORACLE_WALLET_PASSWORD='<wallet-pw>' # if encrypted

python examples/notebook_06_oracle_store.py
python examples/notebook_37_oracle_store.py

If those env vars aren't set the notebook prints the wiring snippet and
exits cleanly — no traceback, no half-initialised state.
Expand Down Expand Up @@ -75,7 +75,7 @@
)


TABLE_NAME = "locus_notebook_06_store"
TABLE_NAME = "locus_notebook_37_store"
USER_ID = "alice"


Expand All @@ -84,7 +84,7 @@ def _missing_env() -> list[str]:


def _print_skip_banner(missing: list[str]) -> None:
print("\n--- Notebook 06: OracleStore-backed cross-thread memory agent ---")
print("\n--- Notebook 37: OracleStore-backed cross-thread memory agent ---")
print(
"Required environment variables not set; skipping the live demo so "
"this file still runs cleanly in CI.\n"
Expand Down
6 changes: 3 additions & 3 deletions examples/notebook_38_oracle_26ai_rag.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@
export OCI_COMPARTMENT=ocid1.compartment.oc1..xxx
export OCI_AUTH_TYPE=api_key # or security_token

python examples/notebook_02_oracle_26ai_rag.py
python examples/notebook_38_oracle_26ai_rag.py

If ``ORACLE_DSN`` / ``ORACLE_PASSWORD`` / ``OCI_COMPARTMENT`` aren't set
the notebook prints the wiring snippet and exits cleanly — no
Expand Down Expand Up @@ -106,7 +106,7 @@ def _missing_env() -> list[str]:


def _print_skip_banner(missing: list[str]) -> None:
print("\n--- Notebook 02: Oracle 26ai RAG agent ---")
print("\n--- Notebook 38: Oracle 26ai RAG agent ---")
print(
"Required environment variables not set; skipping the live demo so "
"this file still runs cleanly in CI.\n"
Expand Down Expand Up @@ -214,7 +214,7 @@ async def main() -> None:
wallet_password=os.environ.get("ORACLE_WALLET_PASSWORD", ""),
# Demo defaults. For production, pre-create the table and pass
# auto_create_table=False so the runtime user only needs DML.
table_name="locus_notebook_06",
table_name="locus_notebook_38",
dimension=1536,
distance_metric="COSINE",
)
Expand Down
12 changes: 6 additions & 6 deletions examples/notebook_39_oracle_adb_loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,11 @@
whole result set in one turn. Switch to ``async for doc in
loader.lazy_load()`` if you're streaming into a UI.
- The same connection envelope (DSN, user, wallet) feeds the loader,
the vector store (notebook 02), the in-DB chunker (notebook 04),
the in-DB embedder (notebook 37), and the checkpointer (07).
the vector store (notebook 38), the in-DB chunker (notebook 40),
the in-DB embedder (notebook 41), and the checkpointer (42).

This notebook is self-contained: it creates a disposable demo table
``locus_notebook_05_articles``, populates a few rows, hands the loader
``locus_notebook_39_articles``, populates a few rows, hands the loader
to an Agent as a tool, asks the agent two domain questions, and drops
the table on the way out.

Expand All @@ -43,7 +43,7 @@
export ORACLE_WALLET=~/.oci/wallets/mydb # directory holding tnsnames.ora
export ORACLE_WALLET_PASSWORD='<wallet-pw>' # if the wallet is encrypted

python examples/notebook_05_oracle_adb_loader.py
python examples/notebook_39_oracle_adb_loader.py

If ``ORACLE_DSN`` / ``ORACLE_PASSWORD`` / ``ORACLE_WALLET`` aren't set
the notebook prints the wiring snippet and exits cleanly — no
Expand Down Expand Up @@ -74,7 +74,7 @@
)


_TABLE = "locus_notebook_05_articles"
_TABLE = "locus_notebook_39_articles"

_DEMO_ROWS = [
(
Expand Down Expand Up @@ -120,7 +120,7 @@ def _missing_env() -> list[str]:


def _print_skip_banner(missing: list[str]) -> None:
print("\n--- Notebook 05: OracleADBLoader agent ---")
print("\n--- Notebook 39: OracleADBLoader agent ---")
print(
"Required environment variables not set; skipping the live demo so "
"this file still runs cleanly in CI.\n"
Expand Down
10 changes: 5 additions & 5 deletions examples/notebook_40_oracle_indb_chunker.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
export ORACLE_WALLET=~/.oci/wallets/mydb # directory holding tnsnames.ora
export ORACLE_WALLET_PASSWORD='<wallet-pw>' # if the wallet is encrypted

python examples/notebook_04_oracle_indb_chunker.py
python examples/notebook_40_oracle_indb_chunker.py

If ``ORACLE_DSN`` / ``ORACLE_PASSWORD`` / ``ORACLE_WALLET`` aren't set
the notebook prints the wiring snippet and exits cleanly — no
Expand Down Expand Up @@ -70,7 +70,7 @@
)


_TABLE = "locus_notebook_04_docs"
_TABLE = "locus_notebook_40_docs"


_LONG_PARAGRAPH = (
Expand All @@ -97,7 +97,7 @@ def _missing_env() -> list[str]:


def _print_skip_banner(missing: list[str]) -> None:
print("\n--- Notebook 04: OracleInDBChunker agent ---")
print("\n--- Notebook 40: OracleInDBChunker agent ---")
print(
"Required environment variables not set; skipping the live demo so "
"this file still runs cleanly in CI.\n"
Expand Down Expand Up @@ -286,7 +286,7 @@ async def chunk_table_rows(limit: int = 100) -> dict:
"""Stream chunks from every row in the demo CLOB table.

Wraps OracleInDBChunker.chunk_column over the
``locus_notebook_04_docs`` table. The source CLOBs never leave
``locus_notebook_40_docs`` table. The source CLOBs never leave
the database — only the chunk records come back. Useful for
an ingest agent that wants to know "how big is this corpus, in
chunks" before committing to write to a vector store.
Expand Down Expand Up @@ -336,7 +336,7 @@ async def chunk_table_rows(limit: int = 100) -> dict:
"\nThe agent picked the right primitive for each request — "
"single-string vs. column-streaming — and every chunk was "
"produced by UTL_TO_CHUNKS inside Oracle 26ai. Pair with "
"OracleInDBEmbeddings (notebook 37) for a fully in-database "
"OracleInDBEmbeddings (notebook 41) for a fully in-database "
"ingest pipeline."
)
finally:
Expand Down
8 changes: 4 additions & 4 deletions examples/notebook_41_oracle_indb_embeddings.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@
export ORACLE_WALLET_PASSWORD='<wallet-pw>' # if the wallet is encrypted
export OCI_INDB_MODEL=ALL_MINILM_L12_V2 # optional — defaults to this

python examples/notebook_37_oracle_indb_embeddings.py
python examples/notebook_41_oracle_indb_embeddings.py

Difficulty: Intermediate. Self-contained — no prior notebook required.
"""
Expand Down Expand Up @@ -130,7 +130,7 @@ def _missing_env() -> list[str]:


def _print_skip_banner(missing: list[str]) -> None:
print("\n--- Notebook 37: OracleInDBEmbeddings agent ---")
print("\n--- Notebook 41: OracleInDBEmbeddings agent ---")
print(
"Required environment variables not set; skipping the live demo so "
"this file still runs cleanly in CI.\n"
Expand Down Expand Up @@ -168,7 +168,7 @@ def _print_skip_banner(missing: list[str]) -> None:


def _print_model_skip_banner(model: str, exc: Exception) -> None:
print("\n--- Notebook 37: OracleInDBEmbeddings agent ---")
print("\n--- Notebook 41: OracleInDBEmbeddings agent ---")
print(
f"The ONNX model {model!r} isn't loaded (or accessible) in the "
"target database, so the live path can't run.\n"
Expand Down Expand Up @@ -259,7 +259,7 @@ async def main() -> None:
password=os.environ["ORACLE_PASSWORD"],
wallet_location=os.path.expanduser(os.environ["ORACLE_WALLET"]),
wallet_password=os.environ.get("ORACLE_WALLET_PASSWORD", ""),
table_name="locus_notebook_10",
table_name="locus_notebook_41",
dimension=_DEFAULT_DIM,
distance_metric="COSINE",
)
Expand Down
8 changes: 4 additions & 4 deletions examples/notebook_42_oracle_26ai_checkpointer.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@
export ORACLE_WALLET=~/.oci/wallets/mydb
export ORACLE_WALLET_PASSWORD='<wallet-pw>' # if the wallet is encrypted

python examples/notebook_03_oracle_26ai_checkpointer.py
python examples/notebook_42_oracle_26ai_checkpointer.py

If ``ORACLE_DSN`` / ``ORACLE_PASSWORD`` aren't set the notebook prints
the wiring snippet and exits cleanly — no traceback, no
Expand Down Expand Up @@ -73,16 +73,16 @@
)


THREAD_ID = "notebook_03_thread"
TABLE_NAME = "locus_notebook_07"
THREAD_ID = "notebook_42_thread"
TABLE_NAME = "locus_notebook_42"


def _missing_env() -> list[str]:
return [name for name in _REQUIRED_ENV if not os.environ.get(name)]


def _print_skip_banner(missing: list[str]) -> None:
print("\n--- Notebook 03: Oracle 26ai durable agent thread ---")
print("\n--- Notebook 42: Oracle 26ai durable agent thread ---")
print(
"Required environment variables not set; skipping the live demo so "
"this file still runs cleanly in CI.\n"
Expand Down
14 changes: 7 additions & 7 deletions examples/notebook_43_oracle_versioned_saver.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

"""Notebook 43: versioned agent history on Oracle Database 26ai.

Notebook 03's ``oracle_checkpointer`` keeps **one row per thread** —
Notebook 42's ``oracle_checkpointer`` keeps **one row per thread** —
``MERGE`` on save means the latest write wins and history is
destructive. That's the right shape for most agent threads. But when
you need a LangGraph-shape *history-preserving* saver — one row per
Expand Down Expand Up @@ -51,7 +51,7 @@
export ORACLE_WALLET=~/.oci/wallets/mydb
export ORACLE_WALLET_PASSWORD='<wallet-pw>' # if encrypted

python examples/notebook_09_oracle_versioned_saver.py
python examples/notebook_43_oracle_versioned_saver.py

If those env vars aren't set the notebook prints the wiring snippet and
exits cleanly — no traceback, no half-initialised state.
Expand Down Expand Up @@ -80,7 +80,7 @@
)


TABLE_PREFIX = "locus_notebook_12"
TABLE_PREFIX = "locus_notebook_43"
THREAD_ID = "agent-incident-001"


Expand All @@ -96,7 +96,7 @@ def _missing_env() -> list[str]:


def _print_skip_banner(missing: list[str]) -> None:
print("\n--- Notebook 09: OracleCheckpointSaver agent ---")
print("\n--- Notebook 43: OracleCheckpointSaver agent ---")
print(
"Required environment variables not set; skipping the live demo so "
"this file still runs cleanly in CI.\n"
Expand All @@ -114,9 +114,9 @@ def _print_skip_banner(missing: list[str]) -> None:
from locus.agent import Agent
from locus.memory.backends import OracleCheckpointSaver

cp = oracle_checkpointer(dsn=..., table_name="locus_notebook_09_live")
cp = oracle_checkpointer(dsn=..., table_name="locus_notebook_43_live")
saver = OracleCheckpointSaver(dsn=..., user=..., wallet_location=...,
table_name="locus_notebook_12")
table_name="locus_notebook_43")
agent = Agent(model=model, system_prompt="...", checkpointer=cp,
checkpoint_every_n_iterations=1)

Expand Down Expand Up @@ -280,7 +280,7 @@ async def main() -> None:
saver = _build_saver()

# Live thread continuity comes from the single-row checkpointer
# (notebook 03's primitive); the versioned saver adds an audit
# (notebook 42's primitive); the versioned saver adds an audit
# trail on top. Both ride the same Autonomous Database wallet.
live_cp = oracle_checkpointer(
dsn=os.environ["ORACLE_DSN"],
Expand Down
Loading
Loading