LangChain Python Integration

Use LangChain with Oracle AI Database to build RAG, semantic search, hybrid retrieval, semantic caching, chat history, and memory-augmented Python applications.

Contents

Choose the Package

The Oracle LangChain ecosystem has two packages that serve different roles. Install one or both packages depending on your use case.

# Database features: vector store, document loading, text splitting, embeddings, and retrievers
pip install -qU langchain-oracledb

# OCI cloud features: LLMs and cloud-hosted embeddings through OCI Generative AI
pip install -qU langchain-oci

# End-to-end RAG on Oracle
pip install -qU langchain-oracledb langchain-oci

# Optional embedding provider used by examples in this guide
pip install -qU langchain-huggingface
Package What It Provides When to Use
langchain-oracledb OracleVS, DistanceStrategy, OracleDocLoader, OracleTextSplitter, OracleEmbeddings, OracleSummary, OracleSemanticCache, OracleChatMessageHistory, OracleHybridSearchRetriever, OracleTextSearchRetriever Store vectors, load and chunk documents, generate in-database embeddings, cache LLM responses, persist chat history, and use retrieval strategies.
langchain-oci ChatOCIGenAI, OCIGenAI, OCIGenAIEmbeddings, ChatOCIModelDeployment Use chat models, completion models, and cloud embeddings through OCI Generative AI.

Both packages can run side by side, and pip resolves dependencies automatically. This guide focuses on langchain-oracledb. For langchain-oci usage, see the OCI Generative AI LangChain documentation.

Migrate from langchain-community

The langchain-oracledb package replaces the older Oracle integrations that lived in the langchain-community package. If you upgrade an existing project, update your imports.

Old Import (langchain-community) New Import (langchain-oracledb)
from langchain_community.vectorstores.oraclevs import OracleVS from langchain_oracledb.vectorstores.oraclevs import OracleVS
from langchain_community.vectorstores import oraclevs from langchain_oracledb.vectorstores import oraclevs
from langchain_community.document_loaders.oracleai import OracleDocLoader from langchain_oracledb.document_loaders.oracleai import OracleDocLoader
from langchain_community.document_loaders.oracleai import OracleTextSplitter from langchain_oracledb.document_loaders.oracleai import OracleTextSplitter
from langchain_community.embeddings.oracleai import OracleEmbeddings from langchain_oracledb.embeddings import OracleEmbeddings
from langchain_community.utilities.oracleai import OracleSummary from langchain_oracledb.utilities.oracleai import OracleSummary
from langchain_community.vectorstores.utils import DistanceStrategy from langchain_oracledb.vectorstores import DistanceStrategy

The class names and constructor signatures remain the same. For the vector store constants, import DistanceStrategy from langchain_oracledb.vectorstores.

The langchain-oracledb package also adds components that are not available in the community package, including OracleSemanticCache, OracleChatMessageHistory, OracleHybridSearchRetriever, OracleTextSearchRetriever, and OracleVectorizerPreference.

Quick Start

Create a minimal end-to-end pipeline that connects, embeds, stores, and searches documents.

import oracledb
from langchain_core.documents import Document
from langchain_huggingface import HuggingFaceEmbeddings
from langchain_oracledb.vectorstores import DistanceStrategy
from langchain_oracledb.vectorstores.oraclevs import OracleVS

# 1. Connect.
connection = oracledb.connect(
    user="myuser",
    password="mypass",
    dsn="localhost:1521/FREEPDB1",
)

# 2. Prepare documents.
docs = [
    Document(
        page_content="Oracle AI Database supports vector search natively.",
        metadata={"source": "docs"},
    ),
    Document(
        page_content="HNSW indexes accelerate approximate nearest-neighbor queries.",
        metadata={"source": "docs"},
    ),
]

# 3. Create embeddings and a vector store.
model = HuggingFaceEmbeddings(model_name="sentence-transformers/all-mpnet-base-v2")
vs = OracleVS.from_documents(
    docs,
    model,
    client=connection,
    table_name="QUICKSTART",
    distance_strategy=DistanceStrategy.COSINE,
)

# 4. Search.
results = vs.similarity_search("How does vector search work?", k=2)
for result in results:
    print(result.page_content)

Tip: For production workloads, use OracleEmbeddings with a database-resident ONNX model to keep embedding computation inside the database.

For a full implementation, see oracle_langchain_example.ipynb.

Connect to Oracle AI Database

The python-oracledb driver supports two modes. Thin mode is the default and connects without Oracle Client libraries. Thick mode enables additional features when Oracle Client libraries are present.

See the python-oracledb feature comparison to decide which mode you need.

import oracledb

username = "<username>"
password = "<password>"
dsn = "<hostname>:<port>/<service_name>"

connection = oracledb.connect(user=username, password=password, dsn=dsn)
print("Connection successful!")

Connect to Oracle Autonomous AI Database with a Wallet

For Oracle Autonomous AI Database connections that use mutual TLS (mTLS), provide wallet details.

connection = oracledb.connect(
    user=username,
    password=password,
    dsn=tns_name,
    config_dir="/path/to/wallet",
    wallet_location="/path/to/wallet",
    wallet_password="wallet_password",
)

With TLS authentication, wallet_location and wallet_password are not required.

The OracleAutonomousDatabaseLoader source documents these connection patterns.

Use Docker for Local Development

Run Oracle AI Database Free locally for development.

docker run -d --name oracle-free \
  -p 1521:1521 -p 5500:5500 \
  -e ORACLE_PASSWORD='OraclePwd_2025' \
  ghcr.io/gvenzl/oracle-free:23.26.0

Then connect to the database.

connection = oracledb.connect(
    user="sys",
    password="OraclePwd_2025",
    dsn="127.0.0.1:1521/FREEPDB1",
)

For a full Docker setup walkthrough, see the Docker setup section in agent_with_memory.ipynb.

Load Documents

Use OracleDocLoader and OracleAutonomousDatabaseLoader to load files, directories, database table content, or query results as LangChain Document objects.

Load Files, Directories, or Database Tables

OracleDocLoader supports more than 150 file formats, including PDF, DOCX, HTML, and TXT, through Oracle Text document processing. You do not need separate loaders for each file format.

from langchain_oracledb.document_loaders.oracleai import OracleDocLoader

# Load from a local file.
loader = OracleDocLoader(conn=connection, params={"file": "/path/to/document.pdf"})
docs = loader.load()

# Load from a directory.
loader = OracleDocLoader(conn=connection, params={"dir": "/path/to/documents/"})
docs = loader.load()

# Load from an Oracle Database table.
loader = OracleDocLoader(
    conn=connection,
    params={
        "owner": "<owner>",
        "tablename": "demo_tab",
        "colname": "data",
    },
)
docs = loader.load()
print(f"Loaded {len(docs)} documents")

For implementation details, see oracleai.py.

Load SQL Query Results as Documents

OracleAutonomousDatabaseLoader runs SQL, including SQL with bind variables, and returns results as LangChain Document objects.

from langchain_oracledb.document_loaders import OracleAutonomousDatabaseLoader

SQL_QUERY = """
SELECT channel_id, channel_desc
FROM sh.channels
WHERE channel_desc = :1
FETCH FIRST 5 ROWS ONLY
"""

doc_loader = OracleAutonomousDatabaseLoader(
    query=SQL_QUERY,
    user=username,
    password=password,
    schema=schema,
    dsn=dsn,
    parameters=["Direct Sales"],
)
docs = doc_loader.load()

For implementation details, see oracleadb_loader.py.

Split Text

OracleTextSplitter uses Oracle AI Database text processing to chunk documents by characters, words, or sentences.

from langchain_oracledb.document_loaders.oracleai import OracleDocLoader
from langchain_oracledb.document_loaders.oracleai import OracleTextSplitter

# Load documents first.
loader = OracleDocLoader(
    conn=connection,
    params={
        "owner": "<owner>",
        "tablename": "demo_tab",
        "colname": "data",
    },
)
docs = loader.load()

# Split by characters. Use a maximum of 500 characters per chunk.
splitter_params = {"split": "chars", "max": 500, "normalize": "all"}

# Split by words. Use a maximum of 100 words per chunk.
# splitter_params = {"split": "words", "max": 100, "normalize": "all"}

# Split by sentences. Use a maximum of 20 sentences per chunk.
# splitter_params = {"split": "sentence", "max": 20, "normalize": "all"}

# Use the default split.
# splitter_params = {"normalize": "all"}

splitter = OracleTextSplitter(conn=connection, params=splitter_params)

all_chunks = []
for doc in docs:
    chunks = splitter.split_text(doc.page_content)
    all_chunks.extend(chunks)

print(f"Total chunks: {len(all_chunks)}")

For implementation details, see oracleai.py.

Generate Embeddings

OracleEmbeddings supports database-resident and third-party embedding providers through one class.

For implementation details, see embeddings/oracleai.py.

Constructor

Use the conn, params, and optional proxy arguments to configure an embeddings provider.

OracleEmbeddings(conn=connection, params={"provider": "...", "model": "..."}, proxy=None)
Parameter Type Description
conn oracledb.Connection Oracle AI Database connection.
params dict Provider configuration.
proxy str Optional HTTP proxy URL for outbound requests, such as "proxy.corp.example.com:80".

Use a Database-Resident ONNX Model

Load an ONNX model into Oracle AI Database and generate embeddings without network round trips.

The load_onnx_model static method takes four positional arguments: the connection, an Oracle Directory name, the ONNX file name, and a model label.

from langchain_oracledb.embeddings import OracleEmbeddings

# Prerequisites:
#   GRANT CREATE MINING MODEL, CREATE PROCEDURE, CREATE ANY DIRECTORY TO <user>;
#   CREATE OR REPLACE DIRECTORY ONNX_DIR AS '/path/to/onnx/files';

# One-time load of an ONNX model into the database.
OracleEmbeddings.load_onnx_model(
    connection,
    "ONNX_DIR",
    "all-minilm-l12-v2.onnx",
    "ALL_MINILM_L12_V2",
)

# Use the database-resident model.
embeddings = OracleEmbeddings(
    conn=connection,
    params={"provider": "database", "model": "ALL_MINILM_L12_V2"},
)

Use OCI Generative AI Embeddings

Configure OracleEmbeddings to call OCI Generative AI through database credentials.

embeddings = OracleEmbeddings(
    conn=connection,
    params={
        "provider": "ocigenai",
        "credential_name": "OCI_CRED",
        "url": "https://inference.generativeai.us-chicago-1.oci.oraclecloud.com/20231130/actions/embedText",
        "model": "cohere.embed-english-light-v3.0",
    },
)

Use Hugging Face Inference API Embeddings

Configure OracleEmbeddings to call Hugging Face through database credentials.

embeddings = OracleEmbeddings(
    conn=connection,
    params={
        "provider": "huggingface",
        "credential_name": "HF_CRED",
        "url": "https://api-inference.huggingface.co/pipeline/feature-extraction/",
        "model": "sentence-transformers/all-MiniLM-L6-v2",
    },
    proxy="proxy.corp.example.com:80",
)

Note: For OCI and Hugging Face providers, first create credentials in Oracle AI Database by using the DBMS_VECTOR_CHAIN helper procedures. See the Oracle AI Vector Search Guide.

Generate Query and Document Embeddings

Use embed_query for one query and embed_documents for batches of documents.

# Single query embedding.
vector = embeddings.embed_query("How does vector search work?")
print(f"Dimension: {len(vector)}")

# Batch document embeddings.
vectors = embeddings.embed_documents(
    [
        "First document text",
        "Second document text",
        "Third document text",
    ]
)
print(f"Embedded {len(vectors)} documents")

Note: OracleEmbeddings internally sets oracledb.defaults.fetch_lobs = False to return strings and bytes instead of LOB locators. If shared code depends on LOB locators, use a separate connection or restore the setting after embedding calls.

Use Third-Party Embeddings Directly

You can also use any LangChain-compatible embedding model outside the database.

from langchain_huggingface import HuggingFaceEmbeddings

model = HuggingFaceEmbeddings(model_name="sentence-transformers/all-mpnet-base-v2")

Note: Hybrid search requires OracleEmbeddings, not third-party embeddings, so that the vectorizer preference matches the embedding configuration.

Use the OracleVS Vector Store

OracleVS is the LangChain vector store integration for Oracle AI Vector Search. It stores document text, metadata, and embedding vectors in one Oracle table.

For implementation details, see oraclevs.py and utils.py.

Understand the Table Schema

When you call OracleVS.from_documents(), it creates a table with this structure.

Column Type Description
id VARCHAR2 Unique document identifier, generated as a UUID.
text CLOB Document text content from page_content.
metadata CLOB Document metadata stored as JSON.
embedding VECTOR Embedding vector.

You can inspect the vector store table directly with SQL.

SELECT id, DBMS_LOB.SUBSTR(text, 100, 1) AS preview, metadata
FROM MY_VECTORS
FETCH FIRST 5 ROWS ONLY;

Create a Vector Store

Create a vector store from documents, or instantiate a vector store over an existing table.

from langchain_oracledb.vectorstores import DistanceStrategy
from langchain_oracledb.vectorstores.oraclevs import OracleVS

# Create from documents. This call creates the table automatically.
vs = OracleVS.from_documents(
    documents,
    embeddings,
    client=connection,
    table_name="MY_VECTORS",
    distance_strategy=DistanceStrategy.COSINE,
)

# Instantiate over an existing table.
vs = OracleVS(connection, table_name="MY_VECTORS", embedding_function=embeddings)

Choose a Distance Strategy

Set the distance strategy to match the embedding model and retrieval response that your application needs.

Strategy Constant Best For
Cosine similarity DistanceStrategy.COSINE Normalized embeddings.
Dot product DistanceStrategy.DOT_PRODUCT Embeddings where magnitude matters.
Euclidean distance DistanceStrategy.EUCLIDEAN_DISTANCE Absolute distance comparison.

Add and Delete Documents

Add raw text with metadata or add LangChain Document objects.

# Add raw texts with metadata.
texts = ["Rohan", "Shailendra"]
metadata = [
    {"id": "100", "link": "Document Example Test 1"},
    {"id": "101", "link": "Document Example Test 2"},
]
vs.add_texts(texts, metadata)

# Add LangChain Document objects.
from langchain_core.documents import Document

new_docs = [
    Document(page_content="New content here", metadata={"id": "200", "source": "api"}),
]
vs.add_documents(new_docs)

# Delete by metadata ID value.
vs.delete(["100"])

Tip: Initialize the vector store with from_documents() for the first batch, then use add_texts() or add_documents() to add more documents incrementally.

For a full implementation, see oracle_langchain_example.ipynb.

Create HNSW and IVF Indexes

Indexes accelerate similarity search over large collections. Oracle AI Vector Search supports HNSW and IVF vector indexes. See Manage Different Categories of Vector Indexes for detailed guidance.

from langchain_oracledb.vectorstores import oraclevs

Create HNSW Indexes

HNSW, or Hierarchical Navigable Small World, is best for high-recall, low-latency approximate nearest-neighbor search.

# Default HNSW index. Uses 8 parallel workers and default accuracy.
oraclevs.create_index(
    connection,
    vs,
    params={"idx_name": "hnsw_idx1", "idx_type": "HNSW"},
)

# HNSW index with target accuracy and parallelism.
oraclevs.create_index(
    connection,
    vs,
    params={
        "idx_name": "hnsw_idx2",
        "idx_type": "HNSW",
        "accuracy": 97,
        "parallel": 16,
    },
)

# HNSW index with power-user parameters.
oraclevs.create_index(
    connection,
    vs,
    params={
        "idx_name": "hnsw_idx3",
        "idx_type": "HNSW",
        "neighbors": 64,
        "efConstruction": 100,
    },
)

Create IVF Indexes

IVF, or inverted file index, is best for very large datasets where memory efficiency matters.

# Default IVF index. Uses 8 parallel workers and default accuracy.
oraclevs.create_index(
    connection,
    vs,
    params={"idx_name": "ivf_idx1", "idx_type": "IVF"},
)

# IVF index with target accuracy and parallelism.
oraclevs.create_index(
    connection,
    vs,
    params={
        "idx_name": "ivf_idx2",
        "idx_type": "IVF",
        "accuracy": 90,
        "parallel": 32,
    },
)

# IVF index with a partition count.
oraclevs.create_index(
    connection,
    vs,
    params={"idx_name": "ivf_idx3", "idx_type": "IVF", "neighbor_part": 64},
)

Review Index Parameters

Use these parameters to configure HNSW and IVF indexes.

Parameter HNSW IVF Description
idx_name Yes Yes Index name.
idx_type "HNSW" "IVF" Index algorithm.
accuracy Yes Yes Target accuracy percentage.
parallel Yes Yes Number of parallel build workers. The default is 8.
neighbors Yes No HNSW graph degree, also known as the M parameter.
efConstruction Yes No HNSW build-time search width.
neighbor_part No Yes IVF partition count.

For implementations of all six index variants, see oracle_langchain_example.ipynb.

Semantic search finds documents based on meaning rather than exact keyword matches. OracleVS provides similarity search and max marginal relevance search methods.

Understand Filtering with Vector Search

Oracle AI Vector Search supports three filtering stages that work with similarity search.

Oracle AI Database automatically determines which filtering strategy to use based on the query, index type, and data distribution. Apply filters through the filter parameter, and Oracle AI Database handles the execution strategy.

Run Basic Similarity Search

Use similarity_search to return matching documents.

results = vs.similarity_search("How are LOBs stored in Oracle Database?", k=2)
for doc in results:
    print(doc.page_content)

Run Similarity Search with Scores

Use similarity_search_with_score to return documents and their distance or relevance scores.

results = vs.similarity_search_with_score("How are LOBs stored?", k=2)
for doc, score in results:
    print(f"Score: {score:.4f} | {doc.page_content[:80]}...")

Run Max Marginal Relevance Search

Max marginal relevance balances relevance with diversity. Use it when you want varied results instead of only the top nearest neighbors.

results = vs.max_marginal_relevance_search(
    "Oracle Database storage",
    k=2,
    fetch_k=20,
    lambda_mult=0.5,
)

Apply Metadata Filters to Search

All search methods support metadata filters.

doc_filter = {"id": "101", "link": "Document Example Test 2"}

results = vs.similarity_search("query", k=2, filter=doc_filter)
results = vs.similarity_search_with_score("query", k=2, filter=doc_filter)
results = vs.max_marginal_relevance_search(
    "query",
    k=2,
    fetch_k=20,
    lambda_mult=0.5,
    filter=doc_filter,
)

See Filter by Metadata for the full filter syntax.

Use OracleVS as a LangChain Retriever

Convert OracleVS into a retriever for use in chains and agents.

from langchain.chains import RetrievalQA
from langchain_openai import ChatOpenAI

retriever = vs.as_retriever(search_kwargs={"k": 5})

llm = ChatOpenAI(model="gpt-4o-mini")
qa = RetrievalQA.from_chain_type(llm=llm, retriever=retriever)
answer = qa.run("What is a tablespace in Oracle Database?")

For a full implementation, see oracle_langchain_example.ipynb.

Full-text search uses the Oracle Text CONTAINS operator for keyword-based retrieval. Use full-text search when users search for specific terms, error codes, product names, or exact phrases.

For implementation details, see text_search.py.

Create a Text Index

Create an Oracle Text index over the built-in text column of an OracleVS table or over another table and column.

from langchain_oracledb.retrievers.text_search import (
    OracleTextSearchRetriever,
    create_text_index,
)

# Index the built-in text column of an existing OracleVS table.
create_text_index(
    connection,
    idx_name="IDX_DOCS_TEXT",
    vector_store=vs,
)

# Index an arbitrary table and column.
create_text_index(
    connection,
    idx_name="IDX_MYDOCS_TEXT",
    table_name="MYDOCS",
    column_name="CONTENT",
)

Build and Use the Text Search Retriever

Use OracleTextSearchRetriever with an OracleVS table or a raw database table.

# When targeting an OracleVS table, returned_columns defaults to ["metadata"].
retriever = OracleTextSearchRetriever(
    vector_store=vs,
    k=5,
    fuzzy=True,
    return_scores=True,
)

docs = retriever.invoke("refund policy")
for doc in docs:
    print(doc.page_content, doc.metadata.get("score"))

When you target a raw table, use client, table_name, and column_name. You can include extra columns through returned_columns.

retriever = OracleTextSearchRetriever(
    client=connection,
    table_name="MYDOCS",
    column_name="CONTENT",
    returned_columns=["title", "author"],
    k=5,
)

Choose a Text Search Mode

Default literal mode treats input as literal text, tokenizes on non-word characters, and rewrites the query as an ACCUM expression. With fuzzy=True, each token is wrapped as FUZZY("token") to handle misspellings.

# Fuzzy matching can find "refund policy" from misspelled input.
docs = retriever.invoke("refnd polcy")

Operator mode passes an Oracle Text expression unchanged. It supports NEAR, ABOUT, AND, OR, NOT, WITHIN, and other Oracle Text operators. In this mode, fuzzy is ignored.

retriever_ops = OracleTextSearchRetriever(
    vector_store=vs,
    operator_search=True,
    return_scores=True,
)

docs = retriever_ops.invoke("NEAR((policy, refund), 2, TRUE)")

For more information, see Oracle Text CONTAINS Query Operators and Fuzzy Matching and Stemming.

Hybrid search combines keyword and semantic signals in one retrieval call. It can produce higher-quality results than either strategy alone.

Note: Hybrid search requires Oracle Database 26ai or later.

For implementation details, see hybrid_search.py.

Understand the Hybrid Search Architecture

The hybrid search pipeline uses three components.

Set Up Hybrid Search

Hybrid search requires OracleEmbeddings, not third-party embeddings, so that the vectorizer preference matches the embedding configuration.

from langchain_core.documents import Document
from langchain_oracledb.embeddings import OracleEmbeddings
from langchain_oracledb.retrievers.hybrid_search import (
    OracleHybridSearchRetriever,
    OracleVectorizerPreference,
    create_hybrid_index,
)
from langchain_oracledb.vectorstores.oraclevs import OracleVS

# Use OracleEmbeddings. This example uses a database-resident model.
embeddings = OracleEmbeddings(
    conn=connection,
    params={"provider": "database", "model": "DB_MODEL"},
)

# Create or load your vector store.
vs = OracleVS(connection, table_name="DOCS", embedding_function=embeddings)

Create a Vectorizer Preference and Hybrid Index

Create a named vectorizer preference, then build the hybrid index.

# Create a named vectorizer preference.
pref = OracleVectorizerPreference.create_preference(
    vector_store=vs,
    preference_name="PREF_DOCS",
)

# Build the hybrid index.
create_hybrid_index(
    connection,
    idx_name="IDX_DOCS_HYB",
    vectorizer_preference=pref,
)

You can also let create_hybrid_index manage a temporary vectorizer preference. Pass vector_store instead of vectorizer_preference.

create_hybrid_index(
    connection,
    idx_name="IDX_DOCS_HYB2",
    vector_store=vs,
    params={"parallel": 8},
)

Search with a Hybrid Retriever

Use OracleHybridSearchRetriever to run hybrid, keyword, or semantic search.

retriever = OracleHybridSearchRetriever(
    vector_store=vs,
    idx_name="IDX_DOCS_HYB",
    search_mode="hybrid",
    k=5,
    return_scores=True,
)

docs = retriever.invoke("refund policy for premium plan")
for doc in docs:
    print(
        doc.page_content,
        doc.metadata.get("score"),
        doc.metadata.get("text_score"),
        doc.metadata.get("vector_score"),
    )

Choose a Hybrid Search Mode

Choose a search mode based on the signal that the user query needs.

Mode Signal When to Use
"hybrid" Keyword and vector combined Default mode for overall recall and precision.
"keyword" Full-text only Users search for exact terms, codes, or names.
"semantic" Vector only Users ask conceptual or natural-language questions.

Tune and Clean Up Hybrid Search

Pass DBMS_HYBRID_VECTOR parameters through the retriever params argument to control score fusion and weighting.

When you no longer need a named vectorizer preference, drop it.

pref.drop_preference()

For more information, see these Oracle AI Vector Search topics:

For a full implementation, see agentic_rag_langchain_oracledb_demo.ipynb.

Filter by Metadata

Oracle AI Vector Search supports pre-filtering, in-filtering, and post-filtering to constrain results before, during, and after similarity search.

Use Filter Operators

Use filter operators to compare metadata values.

Operator Description Example
$eq Equals (=) {"status": "active"}
$ne Not equals (!=) {"status": {"$ne": "archived"}}
$gt Greater than (>) {"age": {"$gt": 30}}
$gte Greater than or equal (>=) {"age": {"$gte": 30}}
$lt Less than (<) {"price": {"$lt": 100}}
$lte Less than or equal (<=) {"price": {"$lte": 100}}
$in Value in array {"category": {"$in": ["A", "B"]}}
$nin Value not in array {"category": {"$nin": ["C"]}}
$between Between two values, inclusive {"score": {"$between": [0.5, 1.0]}}
$exists Field exists {"email": {"$exists": true}}
$startsWith Starts with string {"name": {"$startsWith": "Ora"}}
$hasSubstring Contains substring {"desc": {"$hasSubstring": "vector"}}
$instr Contains substring alias {"desc": {"$instr": "vector"}}
$regex Regular expression {"code": {"$regex": "^ORA-\\d+"}}
$like SQL LIKE pattern {"name": {"$like": "%search%"}}
$all Array contains all items {"tags": {"$all": ["ai", "db"]}}

Use Logical Operators

Use logical operators to combine or negate metadata filters.

Operator Description
$and Logical AND.
$or Logical OR.
$nor Logical NOR.
$not Negates a comparison operator.

Apply a Metadata Filter

Apply a metadata filter to any supported vector search method.

db_filter = {
    "$and": [
        {"id": "101"},
        {
            "$or": [
                {"status": "approved"},
                {"link": "Document Example Test 2"},
                {
                    "$and": [
                        {"status": "approved"},
                        {"link": "Document Example Test 2"},
                    ]
                },
            ]
        },
    ]
}

results = vs.similarity_search("How are LOBs stored?", k=2, filter=db_filter)
results = vs.similarity_search_with_score("How are LOBs stored?", k=2, filter=db_filter)
results = vs.max_marginal_relevance_search(
    "How are LOBs stored?",
    k=2,
    fetch_k=20,
    lambda_mult=0.5,
    filter=db_filter,
)

Use Shorthand Filter Syntax

Omit $and when all conditions must be met. These filters are equivalent.

# Explicit.
{"$and": [{"name": {"$startsWith": "Fred"}}, {"salary": {"$gt": 10000, "$lte": 20000}}]}

# Implicit.
{"name": {"$startsWith": "Fred"}, "salary": {"$gt": 10000, "$lte": 20000}}

Use $not to negate a comparison operator.

{"address.zip": {"$not": {"$eq": "90001"}}}

Use field: value as shorthand for field: {"$eq": value}.

{"animal": "cat"}

For more examples, see the filter test specification.

Use Async Patterns

Key langchain-oracledb components support async operations. Use oracledb.connect_async() for the underlying connection.

Create an Async Connection

Create an async connection before calling async index and retrieval operations.

import oracledb

async_connection = await oracledb.connect_async(
    user=username,
    password=password,
    dsn=dsn,
)

Create Indexes Asynchronously

Use async index creation helpers for Oracle Text and hybrid indexes.

from langchain_oracledb.retrievers.hybrid_search import (
    OracleVectorizerPreference,
    acreate_hybrid_index,
)
from langchain_oracledb.retrievers.text_search import acreate_text_index

# Async text index.
await acreate_text_index(async_connection, idx_name="IDX_TEXT_ASYNC", vector_store=vs)

# Async hybrid index.
pref = await OracleVectorizerPreference.acreate_preference(
    vs,
    preference_name="PREF_ASYNC",
)
await acreate_hybrid_index(
    async_connection,
    idx_name="IDX_HYB_ASYNC",
    vectorizer_preference=pref,
)

Retrieve Asynchronously

Call ainvoke to retrieve documents asynchronously.

# Text search.
docs = await text_retriever.ainvoke("search query")

# Hybrid search.
docs = await hybrid_retriever.ainvoke("search query")

Note: OracleVS similarity search methods inherit async variants from the LangChain base VectorStore class where available.

Cache LLM Responses

Use OracleSemanticCache to cache LLM generations in Oracle AI Database and retrieve prior responses by semantic similarity instead of exact string matching. The cache stores prompts in an Oracle vector table, stores serialized LangChain generations in metadata, and isolates entries by the LangChain llm_string value so different model configurations do not share responses.

For implementation details, see cache.py.

Create a Semantic Cache

Create the cache with an Oracle connection, a LangChain-compatible embedding model, and an optional vector index.

from langchain_oracledb.vectorstores import DistanceStrategy
from langchain_core.globals import set_llm_cache
from langchain_oracledb import OracleSemanticCache

# Reuse the same embedding model you use for semantic search.
cache = OracleSemanticCache(
    client=connection,
    embedding=embeddings,
    table_name="LANGCHAIN_SEMANTIC_CACHE",
    distance_strategy=DistanceStrategy.COSINE,
    create_index_if_missing=True,
    index_name="IDX_LANGCHAIN_SEMANTIC_CACHE",
    score_threshold=0.02,
)

# Install the cache for LangChain LLM calls in this Python process.
set_llm_cache(cache)

Oracle AI Database returns vector distance scores, so lower scores are better. score_threshold is the maximum distance allowed for a cache match. Use a lower value for exact replay, and a higher value when you intentionally want paraphrased prompts to reuse cached answers.

Note: If you install the cache globally for multi-turn agents, start with a tight score_threshold. LangChain cache lookups use the serialized message list, not only the latest user question, so different agent turns can look similar when they share a system prompt and history.

Use the Cache API Directly

Use lookup() and update() directly when you want to cache at the application layer, such as by normalizing the user’s latest question and choosing your own model key.

from langchain_core.outputs import Generation

prompt = "What is Oracle AI Database?"
llm_key = "model=gpt-4o-mini;temperature=0"

cache.update(
    prompt,
    llm_key,
    [Generation(text="Oracle AI Database includes built-in AI and vector features.")],
)

cached = cache.lookup("Explain Oracle AI Database.", llm_key)
if cached is not None:
    print(cached[0].text)

Attach the Cache to a Specific Chat Model

In multi-component applications, attach the cache to a specific chat model instead of installing it globally with set_llm_cache(). Per-model attachment lets you cache a user-facing FAQ chain while leaving an agent’s intermediate LLM calls uncached.

from langchain_openai import ChatOpenAI
from langchain_oracledb import OracleSemanticCache

cache = OracleSemanticCache(
    client=connection,
    embedding=embeddings,
    table_name="LANGCHAIN_SEMANTIC_CACHE",
    score_threshold=0.05,
)

# Cached: a user-facing question-answer chain.
faq_model = ChatOpenAI(model="gpt-4o-mini", cache=cache)

# Not cached: an agent loop whose prompts change every turn.
agent_model = ChatOpenAI(model="gpt-4o-mini")

Per-model attachment is the recommended pattern for agents because cache lookups use the serialized message list, not only the latest user question. Different agent turns can look semantically similar when they share a system prompt and history, which produces false-positive cache matches.

Measure Cache Effectiveness

Verify that the cache is wired correctly with a timed two-call demo on the same prompt. The first call goes to the LLM and writes a cache entry. The second call looks up by embedding similarity and returns the stored response without contacting the LLM.

import time
from langchain_openai import ChatOpenAI
from langchain_oracledb import OracleSemanticCache

cache = OracleSemanticCache(
    client=connection,
    embedding=embeddings,
    table_name="LANGCHAIN_SEMANTIC_CACHE",
    score_threshold=0.05,
)

cache_demo_model = ChatOpenAI(model="gpt-4o-mini", cache=cache, max_tokens=120)

PROMPT = "In one sentence, what is Oracle AI Database?"

t0 = time.perf_counter()
r1 = cache_demo_model.invoke(PROMPT)
miss = time.perf_counter() - t0

t0 = time.perf_counter()
r2 = cache_demo_model.invoke(PROMPT)
hit = time.perf_counter() - t0

assert r1.content == r2.content
print(f"miss: {miss:.2f}s,  hit: {hit:.2f}s,  speedup: {miss / max(hit, 0.001):.1f}x")

The assertion confirms the cache returned the stored response on the second call. If the cache were not connected, the LLM would generate a different response on the second invocation and the assertion would fail.

Manage Cache Entries

Clear all cache entries, or clear only entries for an exact prompt or model configuration.

# Clear one prompt for every model key.
cache.clear(prompt=prompt)

# Clear every prompt for one model key.
cache.clear(llm_string=llm_key)

# Clear the whole cache table.
cache.clear()

# Drop the backing table when you decommission the cache.
OracleSemanticCache.drop_table(connection, "LANGCHAIN_SEMANTIC_CACHE")

Understand the Semantic Cache Table

OracleSemanticCache uses the same underlying vector-table shape as OracleVS. The prompt text is stored as the searchable text, and cache-specific metadata stores hashes and the serialized return value.

Metadata Key Description
prompt_hash SHA-256 hash of the exact prompt text.
llm_string_hash SHA-256 hash of the LangChain model configuration string.
return_val Serialized LangChain Generation objects returned on a cache match.

Store Chat Message History

Use OracleChatMessageHistory to persist LangChain chat messages in Oracle AI Database. The class implements LangChain’s BaseChatMessageHistory, stores one row per message, preserves insertion order with an identity column, and separates conversations by session_id.

For implementation details, see chat_message_histories.py.

Create a Chat History Table

The constructor creates the backing table and session index by default. For production applications, provision the table once with create_tables(), then instantiate per-session history objects with create_table=False and create_index=False.

from langchain_oracledb import OracleChatMessageHistory

OracleChatMessageHistory.create_tables(
    connection,
    "LANGCHAIN_CHAT_HISTORY",
)

The default table schema is:

Column Type Description
id NUMBER identity primary key Preserves message order.
session_id VARCHAR2(255) Application-defined conversation identifier.
message CLOB Serialized LangChain message payload.
created_at TIMESTAMP WITH TIME ZONE Message insert time.

Add, Read, and Clear Messages

Create a history object for each conversation session. Use history_size to return only the most recent messages while keeping older rows in the table.

from langchain_core.messages import AIMessage, HumanMessage, SystemMessage
from langchain_oracledb import OracleChatMessageHistory

history = OracleChatMessageHistory(
    session_id="customer-42",
    client=connection,
    table_name="LANGCHAIN_CHAT_HISTORY",
    create_table=False,
    create_index=False,
    history_size=20,
)

history.add_messages(
    [
        SystemMessage(content="You are a support assistant."),
        HumanMessage(content="My order is late."),
        AIMessage(content="I can help check the order status."),
    ]
)

messages = history.messages
history.clear()

Use RunnableWithMessageHistory

Wrap a chat model or chain with LangChain’s RunnableWithMessageHistory so each invocation automatically loads and writes messages for the configured session.

from langchain_core.messages import HumanMessage
from langchain_core.runnables.history import RunnableWithMessageHistory
from langchain_openai import ChatOpenAI
from langchain_oracledb import OracleChatMessageHistory


def get_session_history(session_id: str) -> OracleChatMessageHistory:
    return OracleChatMessageHistory(
        session_id=session_id,
        client=connection,
        table_name="LANGCHAIN_CHAT_HISTORY",
        create_table=False,
        create_index=False,
        history_size=20,
    )


llm = ChatOpenAI(model="gpt-4o-mini")
chat = RunnableWithMessageHistory(llm, get_session_history)

response = chat.invoke(
    [HumanMessage(content="Summarize my last support request.")],
    config={"configurable": {"session_id": "customer-42"}},
)

Run a Multi-Turn Conversation

Persistence is durable across application restarts. The next time you open the same session_id, the prior turns are still there. Use this pattern to verify that the table is wired correctly.

from langchain_core.messages import AIMessage, HumanMessage
from langchain_oracledb import OracleChatMessageHistory

SESSION_ID = "planner-priya-2026Q3"

# Turn 1: write a question + answer pair.
history = OracleChatMessageHistory(
    session_id=SESSION_ID,
    client=connection,
    table_name="LANGCHAIN_CHAT_HISTORY",
    create_table=False,
    create_index=False,
)
history.add_messages([
    HumanMessage(content="What was our latest soccer merchandise buy recommendation?"),
    AIMessage(content="Conservative volumes on cleats, evening promo support."),
])

# Simulate an application restart by opening the same session anew.
reopened = OracleChatMessageHistory(
    session_id=SESSION_ID,
    client=connection,
    table_name="LANGCHAIN_CHAT_HISTORY",
    create_table=False,
    create_index=False,
)
print(f"Recovered {len(reopened.messages)} turns:")
for message in reopened.messages:
    print(f"  [{type(message).__name__}] {message.content}")

# Turn 2: add a follow-up. Order is preserved by the identity column.
reopened.add_messages([
    HumanMessage(content="And the hydration SKU push?"),
    AIMessage(content="No standing recommendation yet — fresh planner ran an exploratory query."),
])

Each OracleChatMessageHistory instance reads only the rows for its session_id, so multiple sessions can share one table without filtering on the application side.

Manage Sessions

Use plain SQL through the underlying connection to list active sessions or clean up old conversations. The history table has a session index (create_index=True by default) that keeps these queries fast.

# List the distinct sessions stored in the table.
cur = connection.cursor()
cur.execute(
    'SELECT session_id, COUNT(*) AS turns FROM "LANGCHAIN_CHAT_HISTORY" '
    'GROUP BY session_id ORDER BY turns DESC'
)
for session_id, turns in cur.fetchall():
    print(f"  {session_id}: {turns} turns")

# Clear messages for one session.
history_for_session = OracleChatMessageHistory(
    session_id="customer-42",
    client=connection,
    table_name="LANGCHAIN_CHAT_HISTORY",
    create_table=False,
    create_index=False,
)
history_for_session.clear()

# Bulk cleanup: drop conversations older than 30 days.
cur.execute(
    'DELETE FROM "LANGCHAIN_CHAT_HISTORY" '
    "WHERE created_at < SYSTIMESTAMP - INTERVAL '30' DAY"
)
connection.commit()

Use OracleChatMessageHistory.drop_table(connection, "LANGCHAIN_CHAT_HISTORY") to decommission the table entirely.

Combine Cache and Chat History

A real chat application typically uses both primitives together. OracleSemanticCache short-circuits the LLM call when a similar prompt has already been answered. OracleChatMessageHistory persists the conversation so the next session can resume where the user left off. Both layers reside in the same Oracle AI Database, share the same connection, and participate in the same transactions.

from langchain_core.messages import HumanMessage
from langchain_core.runnables.history import RunnableWithMessageHistory
from langchain_openai import ChatOpenAI
from langchain_oracledb import OracleChatMessageHistory, OracleSemanticCache

# Cache: same connection, same embedder.
cache = OracleSemanticCache(
    client=connection,
    embedding=embeddings,
    table_name="LANGCHAIN_SEMANTIC_CACHE",
    score_threshold=0.05,
)

# Chat model: attach the cache to this specific instance.
chat_model = ChatOpenAI(model="gpt-4o-mini", cache=cache)


def get_session_history(session_id: str) -> OracleChatMessageHistory:
    return OracleChatMessageHistory(
        session_id=session_id,
        client=connection,
        table_name="LANGCHAIN_CHAT_HISTORY",
        create_table=False,
        create_index=False,
        history_size=20,
    )


# History: load and write messages around each chat invocation.
chat = RunnableWithMessageHistory(chat_model, get_session_history)

# First turn: cache miss + LLM call + write to history.
response_1 = chat.invoke(
    [HumanMessage(content="What is supply-chain demand planning?")],
    config={"configurable": {"session_id": "planner-priya"}},
)

# Same question, same session: cache hit, no LLM call, history still appended.
response_2 = chat.invoke(
    [HumanMessage(content="What is supply-chain demand planning?")],
    config={"configurable": {"session_id": "planner-priya"}},
)

Both LANGCHAIN_SEMANTIC_CACHE and LANGCHAIN_CHAT_HISTORY are plain Oracle tables. You can join them with your own application tables, back them up with RMAN, and run SQL analytics over them — all the same operational tooling that protects the rest of your data.

Build Agentic RAG with LangGraph

Use LangGraph to build a ReAct agent that dynamically chooses vector, keyword, or hybrid retrieval.

from langchain_core.tools import tool
from langchain_openai import ChatOpenAI
from langgraph.prebuilt import create_react_agent


@tool
def vector_search(query: str) -> str:
    """Search documents by semantic meaning."""
    docs = vs.similarity_search(query, k=3)
    return "\n".join([doc.page_content for doc in docs])


@tool
def keyword_search(query: str) -> str:
    """Search documents by exact keyword match."""
    docs = text_retriever.invoke(query)
    return "\n".join([doc.page_content for doc in docs])


@tool
def hybrid_search(query: str) -> str:
    """Search documents using keyword and semantic signals."""
    docs = hybrid_retriever.invoke(query)
    return "\n".join([doc.page_content for doc in docs])


llm = ChatOpenAI(model="gpt-4o-mini")
agent = create_react_agent(llm, tools=[vector_search, keyword_search, hybrid_search])

response = agent.invoke(
    {
        "messages": [
            {
                "role": "user",
                "content": "What is the refund policy for premium plans?",
            }
        ]
    }
)

For a full implementation, see agentic_rag_langchain_oracledb_demo.ipynb.

Store Agent Memory

Oracle AI Database provides ACID-compliant, semantically searchable storage for agent memory.

Store Memory in Vector Tables

Use OracleVS to store different memory types in separate vector-enabled tables.

from langchain_oracledb.vectorstores import DistanceStrategy
from langchain_oracledb.vectorstores import oraclevs
from langchain_oracledb.vectorstores.oraclevs import OracleVS

# Knowledge base. Stores semantic search over documents.
knowledge_vs = OracleVS.from_documents(
    knowledge_docs,
    embeddings,
    client=connection,
    table_name="AGENT_KNOWLEDGE",
    distance_strategy=DistanceStrategy.COSINE,
)
oraclevs.create_index(
    connection,
    knowledge_vs,
    params={"idx_name": "idx_knowledge", "idx_type": "HNSW"},
)

# Summary memory. Stores compressed conversation summaries.
summary_vs = OracleVS.from_documents(
    [],
    embeddings,
    client=connection,
    table_name="AGENT_SUMMARIES",
    distance_strategy=DistanceStrategy.COSINE,
)

Store Conversation History with OracleChatMessageHistory

Use OracleChatMessageHistory for ordered, session-scoped conversation turns. One table can store many sessions, and history_size limits the number of messages returned to the agent without deleting older rows.

from langchain_core.messages import AIMessage, HumanMessage
from langchain_oracledb import OracleChatMessageHistory

history = OracleChatMessageHistory(
    session_id="thread-1001",
    client=connection,
    table_name="AGENT_CONVERSATION_HISTORY",
    history_size=12,
)

history.add_messages(
    [
        HumanMessage(content="Which supplier is at risk?"),
        AIMessage(content="Supplier A is at risk because lead time increased."),
    ]
)

recent_messages = history.messages

For details, see Store Chat Message History.

Compare Database Memory with Filesystem Memory

Database-backed agent memory provides stronger guarantees than filesystem-backed memory.

Dimension Filesystem Oracle AI Database
Concurrency Race conditions and no database locking. ACID transactions.
Search Filename and path search. Semantic, keyword, and hybrid search.
Scale Single computer. Clustered and distributed deployments.
Durability Local disk failure can cause data loss. Write-ahead logging, backups, and replication.
Access control Operating system permissions. Row-level security and encryption.

For full implementations, see these notebooks:

Summarize Documents

OracleSummary provides document summarization directly within the database by using Oracle text processing or external providers.

The get_summary() method accepts str, Document, list[str], or list[Document] input.

Use the Database Provider

Use the database provider to summarize document text.

from langchain_oracledb.utilities.oracleai import OracleSummary

summarizer = OracleSummary(
    conn=connection,
    params={
        "provider": "database",
        "glevel": "paragraph",
        "numParagraphs": 2,
    },
)

summary = summarizer.get_summary("Long document text goes here...")
print(summary)

Use an External Provider

Use an external provider, such as Cohere, through database credentials.

summarizer = OracleSummary(
    conn=connection,
    params={
        "provider": "cohere",
        "credential_name": "COHERE_CRED",
        "url": "https://api.cohere.com/v1/summarize",
        "model": "command",
    },
    proxy="proxy.corp.example.com:80",
)

summary = summarizer.get_summary("Long document text goes here...")

Summarize Batches

Use get_summary() with lists for batch summarization.

from langchain_core.documents import Document

# Summarize a list of strings.
summaries = summarizer.get_summary(["First document text", "Second document text"])

# Summarize a list of Documents.
summaries = summarizer.get_summary(
    [
        Document(page_content="First document text"),
        Document(page_content="Second document text"),
    ]
)

Note: For external providers, such as Cohere and OCI Generative AI, first create credentials in Oracle AI Database by using DBMS_VECTOR_CHAIN helper procedures.

Troubleshoot Common Issues

Use these troubleshooting patterns for common local development, indexing, and runtime issues.

Docker Container Is Not Ready

Oracle AI Database can take 30 to 60 seconds to initialize after docker run. If a connection fails immediately after you start the container, wait and retry.

import time

import oracledb


def connect_with_retry(user, password, dsn, max_retries=5, delay=10):
    for attempt in range(max_retries):
        try:
            return oracledb.connect(user=user, password=password, dsn=dsn)
        except oracledb.OperationalError:
            if attempt < max_retries - 1:
                print(f"Waiting for database... (attempt {attempt + 1}/{max_retries})")
                time.sleep(delay)
            else:
                raise

Duplicate Key Errors on add_texts

Calling add_texts() with the same metadata id values on a table that enforces uniqueness raises an error. Delete existing records first or catch the exception.

try:
    vs.add_texts(texts, metadata)
except Exception as ex:
    print(f"Duplicate detected: {ex}")

Oracle Text Index Errors

If create_text_index fails, check the Oracle Text error log.

SELECT * FROM ctx_user_index_errors ORDER BY err_timestamp DESC;

macOS ARM64 and Thick Mode

If you need Oracle Client libraries on Apple Silicon, note that Oracle does not provide native ARM64 Instant Client for macOS. Thin mode is the default, does not require these libraries, and works on all platforms.

Only switch to thick mode when you need features listed in the python-oracledb feature comparison.

ORA-01805 Date and Time Errors

The ORA-01805: possible error in date/time operation error can occur with LangChain SQL agents when timezone settings do not match. Set the session timezone explicitly.

cursor = connection.cursor()
cursor.execute("ALTER SESSION SET TIME_ZONE = 'UTC'")

fetch_lobs Side Effects

OracleEmbeddings internally sets oracledb.defaults.fetch_lobs = False. If shared code depends on LOB locators, use a separate connection for embeddings or reset the flag after embedding calls.

oracledb.defaults.fetch_lobs = True

Use these links for package documentation, source code, Oracle AI Vector Search documentation, and notebooks.

Package and API Documentation

Review the package and API documentation for langchain-oracledb, langchain-oci, and the python-oracledb driver.

Resource Link
PyPI: langchain-oracledb pypi.org/project/langchain-oracledb
PyPI: langchain-oci pypi.org/project/langchain-oci
Source: langchain-oracle repository github.com/oracle/langchain-oracle
Source: langchain-oracledb package github.com/oracle/langchain-oracle/tree/main/libs/oracledb
LangChain integration documentation docs.langchain.com/…/vectorstores/oracle
OCI Generative AI integration documentation docs.langchain.com/…/providers/oci
python-oracledb driver python-oracledb.readthedocs.io

Source Code

Review the source code for the main langchain-oracledb components.

Module Link
OracleVS vector store oraclevs.py
Vector store utilities utils.py
OracleHybridSearchRetriever hybrid_search.py
OracleTextSearchRetriever text_search.py
OracleSemanticCache cache.py
OracleChatMessageHistory chat_message_histories.py
OracleEmbeddings embeddings/oracleai.py
OracleDocLoader and OracleTextSplitter document_loaders/oracleai.py
OracleAutonomousDatabaseLoader document_loaders/oracleadb_loader.py
Filter test specification test_oraclevs.py

Oracle AI Vector Search Documentation

Review the Oracle AI Vector Search documentation for vector indexes, hybrid search, and Oracle Text.

Resource Link
Vector index types Manage Different Categories of Vector Indexes
Understand Hybrid Search Understand Hybrid Search
CREATE_PREFERENCE CREATE_PREFERENCE
CREATE_HYBRID_VECTOR_INDEX CREATE_HYBRID_VECTOR_INDEX
DBMS_HYBRID_VECTOR.SEARCH DBMS_HYBRID_VECTOR.SEARCH
Oracle Text CONTAINS operators Oracle Text CONTAINS Query Operators
Fuzzy matching and stemming Fuzzy Matching and Stemming
Thin and thick mode feature comparison python-oracledb feature comparison

Notebooks

Review Oracle and LangChain notebooks for complete examples.

Notebook Description
oracle_langchain_example.ipynb End-to-end ingest, index, search, and filter workflow.
agentic_rag_langchain_oracledb_demo.ipynb Agentic RAG with vector, keyword, and hybrid retrieval through LangGraph.
agent_with_memory.ipynb Persistent agent memory patterns.
fs_vs_dbs.ipynb Filesystem and database memory benchmarks with ACID concurrency tests.
memory_context_engineering_agents.ipynb Memory and context engineering patterns for AI agents.
End-to-End RAG Pipeline Complete RAG pipeline from the LangChain cookbook.
Oracle AI Developer Hub Repository of notebooks, applications, and reference materials.

Community and Learning Resources

Review these resources for labs, articles, and related learning material.

Resource Link
Oracle LiveLabs AI Agent Workshop livelabs.oracle.com Workshop 4185
Multi-Agent RAG with A2A Protocol Oracle Developer Blog
DeepLearning.AI Agent Memory Course Agent Memory: Building Memory-Aware Agents