The RAG and function-calling experiments naturally led to a larger design question: what would an Oracle technical assistant look like if it could use both documentation knowledge and live database information?

I think of the system as four cooperating layers rather than one large prompt: retrieval, controlled database tools, OCI model orchestration, and an API boundary.

1. High-Level Architecture

                    ┌───────────────────────┐
                    │      User / UI        │
                    └───────────┬───────────┘
                                │
                         FastAPI endpoint
                                │
                    ┌───────────▼───────────┐
                    │   Assistant service   │
                    │ retrieve / call tools │
                    └──────┬─────────┬──────┘
                           │         │
                  retrieval│         │live state
                           │         │
                ┌──────────▼───┐ ┌──▼──────────────┐
                │ FAISS index  │ │ Oracle DB tools │
                │ + reranker   │ │ read-only       │
                └──────────┬───┘ └──┬──────────────┘
                           │         │
                           └────┬────┘
                                │
                     OCI Generative AI
                                │
                         grounded answer

2. Route Questions to the Right Source

"What does STARTUP MOUNT do?"
    → documentation retrieval

"What columns are in FACT_SALES?"
    → live metadata tool

"Why might this SQL be slow?"
    → documentation + live plan data

"What is the syntax for DBMS_STATS?"
    → documentation retrieval

A model should not query a database for a question that authoritative documentation can answer, and it should not invent the current schema when a metadata tool can provide exact information.

3. Encapsulate Document Retrieval

class OracleDocRetriever:
    def __init__(self, index, chunks, embedder, reranker):
        self.index = index
        self.chunks = chunks
        self.embedder = embedder
        self.reranker = reranker

    def search(self, question, candidates=12, keep=4):
        q = self.embedder.encode(
            [question],
            normalize_embeddings=True,
            convert_to_numpy=True,
        ).astype("float32")

        _, ids = self.index.search(q, candidates)

        docs = [
            self.chunks[idx]
            for idx in ids[0]
            if idx != -1
        ]

        pairs = [(question, d.text) for d in docs]
        scores = self.reranker.predict(pairs)

        ranked = sorted(
            zip(scores, docs),
            reverse=True,
            key=lambda x: x[0],
        )

        return [doc for _, doc in ranked[:keep]]

4. Prefer Purpose-Specific Database Tools

TOOLS = {
    "lookup_table_columns": lookup_table_columns,
    "get_object_status": get_object_status,
    "get_partition_stats": get_partition_stats,
    "get_sql_plan": get_sql_plan,
}

A few narrow tools are easier to secure and test than one unrestricted run_any_sql function.

5. Example: Object Status Tool

def get_object_status(object_name):
    sql = """
        SELECT object_name,
               object_type,
               status
        FROM user_objects
        WHERE object_name = :name
    """

    with connection.cursor() as cur:
        cur.execute(
            sql,
            name=object_name.upper()
        )

        return [
            {
                "name": row[0],
                "type": row[1],
                "status": row[2],
            }
            for row in cur
        ]

6. Keep Secrets Outside Model Context

LLM may see:
    tool name
    tool description
    allowed arguments
    tool result

LLM should not see:
    DB password
    OCI private key
    wallet password
    connection string
    privileged shell environment

The model gets a capability abstraction, not the credential that implements it.

7. Build Grounded Context from Retrieved Sources

docs = retriever.search(question)

context = "\\n\\n".join(
    f"{d.source} / {d.section}\\n{d.text}"
    for d in docs
)

user_text = f"""
Use the Oracle documentation below to answer the question.
Do not invent unsupported syntax.

DOCUMENTATION
-------------
{context}

QUESTION
--------
{question}
"""

8. Use One OCI Tool-Orchestration Loop

def run_assistant(messages, tools):
    while True:
        request = GenericChatRequest(
            messages=messages,
            tools=tools,
            tool_choice=ToolChoiceAuto(),
            temperature=0.1,
            max_tokens=1200,
        )

        details = ChatDetails(
            compartment_id=COMPARTMENT_ID,
            serving_mode=OnDemandServingMode(
                model_id=MODEL_ID
            ),
            chat_request=request,
        )

        response = client.chat(details)

        msg = (
            response.data.chat_response
            .choices[0]
            .message
        )

        messages.append(msg)

        if not msg.tool_calls:
            return msg.content[0].text

        for call in msg.tool_calls:
            result = execute_tool(call)

            messages.append(
                ToolMessage(
                    tool_call_id=call.id,
                    content=[
                        TextContent(
                            text=json.dumps(result)
                        )
                    ],
                )
            )

The model can continue requesting tools until it has enough evidence to produce a normal assistant response.

9. Put FastAPI in Front of the Assistant

from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()

class AskRequest(BaseModel):
    question: str

@app.post("/ask")
def ask(req: AskRequest):
    answer = assistant.ask(req.question)

    return {
        "question": req.question,
        "answer": answer,
    }

This keeps the UI and consumers independent from OCI-specific implementation details. The service can later add authentication, trace storage, an MCP boundary, or another model provider without redesigning the front end.

10. Log the Retrieval and Tool Trace

{
  "question": "...",
  "retrieved_chunks": [
    {
      "source": "...",
      "section": "...",
      "score": 0.81
    }
  ],
  "tool_calls": [
    {
      "name": "get_sql_plan",
      "arguments": {}
    }
  ],
  "tool_results": [],
  "model": "...",
  "final_answer": "..."
}

This makes incorrect answers diagnosable. I can ask whether the problem came from retrieval, ranking, tool selection, tool output, or final interpretation.

11. Why Oracle Is a Good Domain for This Pattern

Oracle systems expose a large amount of structured evidence: catalog views, dynamic performance views, object metadata, optimizer statistics, execution plans, and authoritative technical documentation. That makes it possible to build an assistant whose answer can be grounded in both static knowledge and live system state.

What I Learned

A useful enterprise assistant is not simply an LLM with a large prompt. The stronger architecture combines RAG for authoritative knowledge, tightly controlled tools for live state, OCI Generative AI for interpretation and orchestration, and an API layer that keeps credentials and infrastructure details outside the model context. Every retrieval and tool action should also be inspectable so incorrect answers can be debugged like any other software system.