diff --git a/apps/oracle-langchain-js-rag/.env.example b/apps/oracle-langchain-js-rag/.env.example new file mode 100644 index 00000000..976df0c2 --- /dev/null +++ b/apps/oracle-langchain-js-rag/.env.example @@ -0,0 +1,38 @@ +# Oracle AI Database connection. +# Use one of these connection targets: +# 1. Local Oracle Database Free / Oracle AI Database: +# ORACLE_CONNECT_STRING=localhost:1521/FREEPDB1 +# 2. Autonomous AI Database: +# ORACLE_CONNECT_STRING= +# ORACLE_WALLET_LOCATION= +# ORACLE_WALLET_PASSWORD= +# +# Use ORACLE_* names for this app; DB_* aliases are also accepted by the code. +ORACLE_USER= +ORACLE_PASSWORD= +ORACLE_CONNECT_STRING= +# Alias accepted by the app if this is the name you normally use. +ORACLE_DSN= + +# Optional wallet settings for Autonomous Database mTLS connections. +ORACLE_WALLET_LOCATION= +ORACLE_WALLET_PASSWORD= + +# Demo table used by OracleVS. +ORACLE_TABLE_NAME=LC_JS_RAG_DEMO +ORACLE_CREATE_VECTOR_INDEX=false + +# Optional. If omitted, the app returns a grounded extractive answer from retrieved context. +OPENAI_API_KEY= +OPENAI_MODEL=gpt-4o-mini +# Alias accepted by the app if this is the model variable you already use. +OAMP_LLM_MODEL=gpt-5.4 + +# Optional placeholders for teams that keep Oracle embedding settings in .env. +# This sample uses deterministic demo embeddings for repeatable review runs. +ORACLE_DB_EMBEDDING_MODE=demo +ORACLE_DB_EMBEDDING_DIMENSION=12 + +# Local app ports. +PORT=8787 +VITE_API_BASE_URL=http://localhost:8787 diff --git a/apps/oracle-langchain-js-rag/.gitignore b/apps/oracle-langchain-js-rag/.gitignore new file mode 100644 index 00000000..ac792a15 --- /dev/null +++ b/apps/oracle-langchain-js-rag/.gitignore @@ -0,0 +1,11 @@ +node_modules/ +dist/ +.vite/ +.env +.env.* +!.env.example +*.log +.DS_Store +coverage/ +*.tsbuildinfo + diff --git a/apps/oracle-langchain-js-rag/Dockerfile b/apps/oracle-langchain-js-rag/Dockerfile new file mode 100644 index 00000000..9f4c08ac --- /dev/null +++ b/apps/oracle-langchain-js-rag/Dockerfile @@ -0,0 +1,27 @@ +FROM node:22-alpine AS build + +WORKDIR /app +RUN corepack enable + +COPY package.json pnpm-lock.yaml* pnpm-workspace.yaml tsconfig.json ./ +COPY apps ./apps +COPY packages ./packages +COPY samples ./samples +COPY services ./services +COPY scripts ./scripts + +RUN pnpm install --frozen-lockfile=false +RUN pnpm build + +FROM node:22-alpine AS runtime + +WORKDIR /app +ENV NODE_ENV=production +ENV PORT=8787 +ENV WEB_DIST_DIR=/app/apps/web/dist + +RUN corepack enable +COPY --from=build /app ./ + +EXPOSE 8787 +CMD ["pnpm", "start"] diff --git a/apps/oracle-langchain-js-rag/README.md b/apps/oracle-langchain-js-rag/README.md new file mode 100644 index 00000000..87cb93b5 --- /dev/null +++ b/apps/oracle-langchain-js-rag/README.md @@ -0,0 +1,172 @@ +# Oracle AI Database + LangChain.js RAG Assistant + +Companion TypeScript application for the article [_Build a LangChain.js RAG Assistant with Oracle AI Database_](./docs/article.md). + +This sample shows JavaScript and TypeScript developers how to use LangChain.js with Oracle AI Database as the vector store. It is intentionally a runnable Node.js app, not a notebook: a React frontend calls a Hono API, the API runs a LangChain.js RAG workflow, and `@oracle/langchain-oracledb` stores and searches vectors in Oracle AI Database through `OracleVS`. + +The sample answers developer questions about Oracle AI Database, OracleVS, metadata filters, MMR retrieval, vector indexes, and common Oracle connection errors from a seeded markdown knowledge base. + +## Pipeline + +1. Load markdown files from `samples/knowledge-base`. +2. Split them into section-level LangChain `Document` chunks with metadata. +3. Embed the chunks with a deterministic TypeScript `Embeddings` implementation for repeatable local runs. +4. Store text, embeddings, and JSON metadata in Oracle AI Database with `OracleVS.fromDocuments`. +5. Retrieve context with similarity search, similarity search with scores, metadata filters, or MMR. +6. Generate a grounded answer from the retrieved OracleVS context, or return an extractive answer when no LLM key is configured. +7. Inspect the Oracle table, columns, row count, vector index, query vector preview, retrieved chunks, and representative vector-search SQL in the browser UI. + +## Prerequisites + +- Node.js 22 or later +- pnpm 10 or later +- One Oracle Database target with vector support: local Oracle Database Free / Oracle AI Database or Autonomous AI Database +- A schema that can create, insert into, select from, and drop demo tables +- Optional: `OPENAI_API_KEY` for generated grounded answers + +## Quickstart + +```bash +pnpm install +cp .env.example .env +``` + +Choose one Oracle connection target and fill in `.env`: + +| Target | When to use it | Connection values | +| --- | --- | --- | +| Local Oracle Database Free / Oracle AI Database | Local development with a container or local database listener | `ORACLE_CONNECT_STRING=localhost:1521/FREEPDB1` | +| Autonomous AI Database | Cloud database with wallet-based mTLS | `ORACLE_CONNECT_STRING=`, plus `ORACLE_WALLET_LOCATION` and usually `ORACLE_WALLET_PASSWORD` | + +For example, a local Oracle Database Free setup usually looks like: + +```env +ORACLE_USER= +ORACLE_PASSWORD= +ORACLE_CONNECT_STRING=localhost:1521/FREEPDB1 +ORACLE_TABLE_NAME=LC_JS_RAG_DEMO +ORACLE_CREATE_VECTOR_INDEX=false +``` + +The same app also accepts `DB_USER`, `DB_PASSWORD`, `DB_CONNECT_STRING`, `DB_DSN`, and `ORACLEDB_CONNECTION_STRING` aliases if those are the names copied from another tutorial. + +Check the database connection: + +```bash +pnpm db:check +``` + +Seed the OracleVS table: + +```bash +pnpm seed +``` + +Run the API in one terminal: + +```bash +pnpm dev:api +``` + +Run the web app in another terminal: + +```bash +pnpm dev +``` + +Open: + +```text +http://localhost:5173 +``` + +## Container Deployment + +The Dockerfile builds the React app and the Hono API into one Node.js image. At runtime the API serves the compiled web app from the same origin, so `VITE_API_BASE_URL` can be omitted unless the browser UI must call a separate API host. + +Build the image: + +```bash +docker build -t oracle-langchain-js-rag . +``` + +Run it with the same environment values used for local development: + +```bash +docker run --env-file .env -p 8787:8787 oracle-langchain-js-rag +``` + +Open: + +```text +http://localhost:8787 +``` + +The container expects the Oracle database target from `.env` to be reachable from inside the container. For a local database running on the host, use the host address that your container runtime exposes rather than `localhost`. + +## Configuration + +| Variable | Required | Description | +| --- | --- | --- | +| `ORACLE_USER` | yes | Oracle schema username | +| `ORACLE_PASSWORD` | yes | Oracle schema password | +| `ORACLE_CONNECT_STRING` | yes | Oracle connect string, service name, descriptor, or TCPS DSN | +| `ORACLE_DSN` | alias | Accepted as an alias for `ORACLE_CONNECT_STRING` | +| `ORACLE_WALLET_LOCATION` | no | Wallet directory for mTLS Autonomous Database connections | +| `ORACLE_WALLET_PASSWORD` | no | Wallet password when needed by node-oracledb thin mode | +| `ORACLE_TABLE_NAME` | no | OracleVS table name. Defaults to `LC_JS_RAG_DEMO` | +| `ORACLE_CREATE_VECTOR_INDEX` | no | Set `true` to attempt HNSW vector index creation during seed | +| `OPENAI_API_KEY` | no | Enables generated grounded answers through `@langchain/openai` | +| `OPENAI_MODEL` | no | Chat model for answer generation. Defaults to `gpt-4o-mini` | +| `OAMP_LLM_MODEL` | alias | Accepted as a model-name alias if you already use this variable | +| `PORT` | no | API port. Defaults to `8787` | +| `VITE_API_BASE_URL` | no | Web app API base URL. Defaults to same-origin; `.env.example` sets `http://localhost:8787` for local Vite development | + +`DB_USER`, `DB_PASSWORD`, `DB_CONNECT_STRING`, `DB_DSN`, and `ORACLEDB_CONNECTION_STRING` are also accepted as aliases for local conventions. + +## Repo Layout + +```text +apps/web React + Vite UI for retrieval controls, traces, answers, and Oracle inspection +services/api Hono API exposing status, corpus, inspect, seed, reset, and ask endpoints +packages/core Knowledge-base loading, embeddings, retrieval, answer generation, and trace shaping +packages/db Oracle config, node-oracledb pool, OracleVS helpers, table inspection +packages/shared Shared TypeScript types for API responses +scripts/ CLI scripts for db:check, seed, reset, and cleanup +samples/ Markdown knowledge-base files loaded into OracleVS +docs/ Long-form article, local development guide, diagrams, and screenshots +``` + +## Scripts + +| Command | Description | +| --- | --- | +| `pnpm db:check` | Validate environment variables and test the Oracle connection | +| `pnpm seed` | Reset and load the markdown knowledge base into OracleVS | +| `pnpm reset` | Drop the demo OracleVS table | +| `pnpm dev:api` | Run the Hono API on `:8787` | +| `pnpm dev` | Run the Vite web app on `:5173` | +| `pnpm typecheck` | Type-check all workspace packages | +| `pnpm test` | Run unit tests | +| `pnpm build` | Build packages, API, and web app | +| `pnpm check` | Run typecheck, tests, and build | +| `pnpm clean` | Remove generated build outputs and caches | +| `pnpm clean:deps` | Remove generated outputs plus `node_modules` folders | + +## Docs + +- [Article: Build a LangChain.js RAG Assistant with Oracle AI Database](./docs/article.md) +- [Local development](./docs/local-development.md) + +## What Is Intentionally Not Here + +- Production authentication. The sample is local-first. +- A production embedding provider. The deterministic demo embeddings keep runs reproducible; swap in OpenAI, OCI Generative AI, or another LangChain.js `Embeddings` implementation for real applications. +- A production table lifecycle. The seed script drops and recreates the configured demo table, so do not point `ORACLE_TABLE_NAME` at a production table. +- A separate vector database. Oracle AI Database is the vector store in this sample. + +## Learn More + +- [Oracle LangChain JavaScript integration guide](https://docs.oracle.com/en/database/oracle/oracle-database/26/aintg/langchain-oracledb-integration-guide/langchain-javascript.html) +- [LangChain.js Oracle AI Database vector store docs](https://docs.langchain.com/oss/javascript/integrations/vectorstores/oracleai) +- [Source: oracle/langchain-oracle](https://github.com/oracle/langchain-oracle/tree/main/libs/js/langchain-oracledb) diff --git a/apps/oracle-langchain-js-rag/apps/web/index.html b/apps/oracle-langchain-js-rag/apps/web/index.html new file mode 100644 index 00000000..e7da4bbb --- /dev/null +++ b/apps/oracle-langchain-js-rag/apps/web/index.html @@ -0,0 +1,12 @@ + + + + + + LangChain.js RAG with Oracle AI Database + + +
+ + + diff --git a/apps/oracle-langchain-js-rag/apps/web/package.json b/apps/oracle-langchain-js-rag/apps/web/package.json new file mode 100644 index 00000000..0d43c169 --- /dev/null +++ b/apps/oracle-langchain-js-rag/apps/web/package.json @@ -0,0 +1,25 @@ +{ + "name": "@oracle-langchain-js-rag/web", + "version": "0.1.0", + "private": true, + "type": "module", + "scripts": { + "dev": "vite --host 0.0.0.0 --port 5173", + "build": "tsc -p tsconfig.json --noEmit && vite build", + "preview": "vite preview --host 0.0.0.0 --port 5173", + "typecheck": "tsc -p tsconfig.json --noEmit" + }, + "dependencies": { + "@oracle-langchain-js-rag/shared": "workspace:*", + "@vitejs/plugin-react": "^4.3.4", + "lucide-react": "^0.468.0", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "vite": "^6.0.5" + }, + "devDependencies": { + "@types/react": "^19.0.2", + "@types/react-dom": "^19.0.2", + "typescript": "^5.9.3" + } +} diff --git a/apps/oracle-langchain-js-rag/apps/web/src/App.tsx b/apps/oracle-langchain-js-rag/apps/web/src/App.tsx new file mode 100644 index 00000000..4c4198d4 --- /dev/null +++ b/apps/oracle-langchain-js-rag/apps/web/src/App.tsx @@ -0,0 +1,696 @@ +import { useEffect, useMemo, useState } from "react"; +import { + AlertTriangle, + BookOpen, + Boxes, + CheckCircle2, + ChevronRight, + Code2, + Database, + FileText, + Gauge, + GitBranch, + Layers3, + Loader2, + Network, + Play, + RefreshCw, + RotateCcw, + Search, + SlidersHorizontal, + Table2, +} from "lucide-react"; + +import type { + AppStatus, + AskResponse, + CorpusResponse, + OracleInspectionResponse, + RetrievalMode, + SeedResponse, + WorkflowStep, +} from "@oracle-langchain-js-rag/shared"; + +import { + askQuestion, + getCorpus, + getOracleInspection, + getStatus, + resetCorpus, + seedCorpus, +} from "./api"; + +const sampleQuestions = [ + "How do I use OracleVS with LangChain.js in a TypeScript RAG app?", + "How do metadata filters improve retrieval?", + "How should I fix ORA-28001 when running this Node.js sample?", + "When should I use MMR instead of similarity search?", +]; + +const defaultQuestion = sampleQuestions[0] ?? ""; +const tabs = ["Technical Flow", "Live Assistant", "Oracle Table"] as const; + +const architectureNodes = [ + { + id: "ui", + label: "React UI", + headline: "Developer asks a question", + detail: "The front end collects the question, metadata filter, retrieval mode, and top K so the developer can compare RAG behavior without changing code.", + capability: "LangChain workflow is exposed as a runnable TypeScript app, not a notebook.", + file: "apps/web/src/App.tsx", + icon: Search, + }, + { + id: "api", + label: "Node.js API", + headline: "Hono routes orchestrate the request", + detail: "The API validates input, loads root environment configuration, checks Oracle, and calls the shared RAG service.", + capability: "This is the intended JavaScript/TypeScript runtime for the sample.", + file: "services/api/src/app.ts", + icon: Network, + }, + { + id: "langchain", + label: "LangChain.js", + headline: "Documents, embeddings, retrievers, and LLM call", + detail: "The core package builds LangChain Document chunks, creates embeddings, runs OracleVS retrieval, and prepares grounded context for the model.", + capability: "Demonstrates the RAG building blocks developers expect from LangChain.js.", + file: "packages/core/src/rag.ts", + icon: GitBranch, + }, + { + id: "embeddings", + label: "Embeddings", + headline: "Text becomes vectors", + detail: "The demo uses deterministic TypeScript embeddings for reproducible local runs, while the code path can be swapped for OpenAI, OCI, or another LangChain embedding implementation.", + capability: "Shows the embedding interface and the query vector preview.", + file: "packages/core/src/demoEmbeddings.ts", + icon: Gauge, + }, + { + id: "oracle", + label: "OracleVS", + headline: "Oracle AI Database stores vectors and metadata", + detail: "OracleVS persists text, metadata, and vector embeddings in an Oracle table, then performs similarity or MMR retrieval with optional metadata filtering.", + capability: "Highlights Oracle AI Database as the vector store behind LangChain.js.", + file: "packages/db/src/vectorStore.ts", + icon: Database, + }, + { + id: "answer", + label: "Grounded Answer", + headline: "The LLM answers from retrieved evidence", + detail: "The answer step receives only retrieved OracleVS evidence and returns a practical developer response with source chunk citations.", + capability: "Connects retrieval to LLM generation while keeping the evidence inspectable.", + file: "packages/core/src/rag.ts", + icon: CheckCircle2, + }, +] as const; + +type BusyAction = "status" | "seed" | "reset" | "ask" | "inspect" | null; +type Tab = (typeof tabs)[number]; +type ArchitectureNode = (typeof architectureNodes)[number]; +type ArchitectureNodeId = ArchitectureNode["id"]; + +function StatusBadge({ ok, label }: { ok: boolean; label: string }) { + return ( + + {ok ? : } + {label} + + ); +} + +function LoadingIcon({ show }: { show: boolean }) { + return show ? : null; +} + +function StepPill({ step }: { step: WorkflowStep }) { + const ok = step.status === "PASS" || step.status === "READY"; + return ( +
  • + + {step.step} + {step.status} +

    {step.detail}

    +
  • + ); +} + +function TechnicalFlowPanel({ + corpus, + result, + status, +}: { + corpus: CorpusResponse | null; + result: AskResponse | null; + status: AppStatus | null; +}) { + const [activeNodeId, setActiveNodeId] = useState("langchain"); + const activeNode = architectureNodes.find((node) => node.id === activeNodeId) ?? architectureNodes[2]; + + return ( +
    +
    +
    +

    Interactive technical map

    +

    LangChain.js RAG running against Oracle AI Database

    +

    + This view follows one developer question through the TypeScript app: UI input, Node.js API, + LangChain.js retrieval, OracleVS vector search, and grounded LLM answer. +

    +
    + Documents + Embeddings + OracleVS + Similarity + MMR + Metadata filters + Grounded answer +
    +
    + +
    +
    + + {architectureNodes.map((node, index) => { + const Icon = node.icon; + return ( + + ); + })} +
    +
    + +
    +
    +
    + +

    {activeNode.headline}

    +
    +

    {activeNode.detail}

    +
    +
    +
    Capability
    +
    {activeNode.capability}
    +
    +
    +
    Code path
    +
    {activeNode.file}
    +
    +
    +
    + +
    +
    + +

    Runtime snapshot

    +
    +
    +
    +
    Oracle
    +
    {status?.database.detail ?? "Waiting for API status"}
    +
    +
    +
    Knowledge base
    +
    {corpus ? `${corpus.totalDocuments} chunks from ${corpus.totalSources} sources` : "Loading corpus"}
    +
    +
    +
    Latest retrieval
    +
    {result ? `${result.retrieved.length} chunks via ${result.retrievalMode}` : "Run a question in Live Assistant"}
    +
    +
    +
    +
    +
    + ); +} + +function LiveAssistantPanel({ + busy, + categories, + category, + corpus, + k, + question, + result, + retrievalMode, + setCategory, + setK, + setQuestion, + setRetrievalMode, + onAsk, +}: { + busy: BusyAction; + categories: string[]; + category: string; + corpus: CorpusResponse | null; + k: number; + question: string; + result: AskResponse | null; + retrievalMode: RetrievalMode; + setCategory: (value: string) => void; + setK: (value: number) => void; + setQuestion: (value: string) => void; + setRetrievalMode: (value: RetrievalMode) => void; + onAsk: () => void; +}) { + return ( +
    +
    +
    { + event.preventDefault(); + onAsk(); + }} + > +
    + +

    Ask the TypeScript RAG assistant

    +
    + +