-
Notifications
You must be signed in to change notification settings - Fork 91
Add CopilotKit retrieval integration cookbook example #367
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
4b900af
0d01af9
81658b5
ac78592
46b46d7
4f1d726
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| # Moss Credentials (get from https://portal.usemoss.dev) | ||
| MOSS_PROJECT_ID=your_moss_project_id | ||
| MOSS_PROJECT_KEY=your_moss_project_key | ||
| MOSS_INDEX_NAME=your_moss_index_name | ||
|
|
||
| # LLM Provider Key (required for CopilotKit backend runtime) | ||
| OPENAI_API_KEY=your_openai_api_key |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| { | ||
| "extends": "next/core-web-vitals" | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| node_modules/ | ||
| .next/ | ||
| .env | ||
| *.tsbuildinfo |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,154 @@ | ||
| # In-App Copilots Grounding — MOSS + CopilotKit | ||
|
|
||
| Ground [CopilotKit](https://github.com/CopilotKit/CopilotKit) in-app copilots with MOSS's semantic search runtime (sub-10ms when querying a locally loaded index) to answer user queries using your own knowledge base. | ||
|
|
||
| **How it works:** | ||
|
|
||
| 1. The frontend registers a custom Moss search tool with CopilotKit using a custom React hook `useMossRetrieval`. | ||
| 2. When the user asks a question in the in-app chatbot, the CopilotKit agent detects if the question requires document retrieval. | ||
| 3. The agent triggers the retrieval action, sending the query to the browser-side handler. | ||
| 4. The client queries a secure Next.js API route (`/api/moss/query`), keeping credentials hidden from the browser. | ||
| 5. The backend uses the Node-based `@moss-dev/moss` SDK to retrieve matching documents from your Moss index. | ||
| 6. The retrieved documents are fed back to the CopilotKit agent, grounding the LLM's response in accurate, real-time context. | ||
|
|
||
| ``` | ||
| You: What is your refund policy? | ||
|
|
||
| [Agent triggers Moss search: "refund policy"] | ||
| [Moss returns: "Refunds allowed on unused items in original packaging within 30 days..."] | ||
|
|
||
| Agent: You can return any unused item in its original packaging within 30 days of purchase for a full refund. Note that return shipping costs are covered by the customer. | ||
| ``` | ||
|
|
||
| --- | ||
|
|
||
| ## Project Structure | ||
|
|
||
| ``` | ||
| copilotkit/ | ||
| ├── app/ | ||
| │ ├── api/ | ||
| │ │ ├── copilotkit/ | ||
| │ │ │ └── route.ts # CopilotKit backend runtime orchestrator | ||
| │ │ └── moss/ | ||
| │ │ └── query/ | ||
| │ │ └── route.ts # Secure server-side Moss search query endpoint (with mock fallback) | ||
| │ ├── globals.css # Premium dark-mode UI styling | ||
| │ ├── layout.tsx # App root layout with font and style imports | ||
| │ ├── page.tsx # App entry page with developer dashboard & CopilotChat | ||
| │ └── use-moss-retrieval.ts # React hook mapping the Moss query action to CopilotKit | ||
| ├── .env.example # Environment variables template | ||
| ├── next.config.ts # Next.js configurations allowing native Node bindings | ||
| ├── package.json # Project dependencies & scripts | ||
| ├── README.md # This guide | ||
| └── tsconfig.json # TypeScript configurations | ||
| ``` | ||
|
|
||
| --- | ||
|
|
||
| ## Setup | ||
|
|
||
| ### 1. Install dependencies | ||
|
|
||
| Install the project dependencies using npm (run in this directory): | ||
|
|
||
| ```bash | ||
| npm install | ||
| ``` | ||
|
|
||
| ### 2. Set environment variables | ||
|
|
||
| Copy the environment variables template and configure your keys: | ||
|
|
||
| ```bash | ||
| cp .env.example .env | ||
| ``` | ||
|
|
||
| Open `.env` and fill in the values: | ||
|
|
||
| | Variable | Required | Description | | ||
| |---|---|---| | ||
| | `OPENAI_API_KEY` | **Yes** | Your OpenAI API Key (required by CopilotKit to talk to the LLM agent). | | ||
| | `MOSS_PROJECT_ID` | Optional | Your Moss Project ID from the [Moss Portal](https://portal.usemoss.dev). | | ||
| | `MOSS_PROJECT_KEY` | Optional | Your Moss Project Key from the [Moss Portal](https://portal.usemoss.dev). | | ||
| | `MOSS_INDEX_NAME` | Optional | Name of the Moss index to run queries against. | | ||
|
|
||
| > [!NOTE] | ||
| > **Mock Mode Fallback:** If you do not provide Moss credentials (`MOSS_PROJECT_ID`, `MOSS_PROJECT_KEY`), the server-side API route will automatically fall back to running query searches against a set of built-in mock documents (Refund Policy, Office Hours, Support Contact, Moss Info). This allows you to explore and test the entire integration end-to-end immediately without needing a Moss account. | ||
|
|
||
| --- | ||
|
|
||
| ## Usage | ||
|
|
||
| Start the development server: | ||
|
|
||
| ```bash | ||
| npm run dev | ||
| ``` | ||
|
|
||
| Open [http://localhost:3000](http://localhost:3000) in your browser. | ||
|
|
||
| ### What to try: | ||
| 1. **View the Control Room:** Observe the status badge showing whether the application is running in **Mock Mode** or **Connected to Moss Cloud**. | ||
| 2. **Interact with the Chat:** Open the chat window on the right side and ask a question such as: | ||
| - *"What is your refund policy?"* | ||
| - *"Where is your corporate headquarters?"* | ||
| - *"How can I contact customer support?"* | ||
| - *"What is Moss?"* | ||
| 3. **Watch the Retrieval Event Terminal:** As soon as you press send, the **Retrieval Event Terminal** on the bottom-left will stream status updates in real-time, showing the CopilotKit agent calling the Moss search tool and retrieving matching documents. | ||
| 4. **Test Direct Search:** Use the **Direct Moss Query Sandbox** on the top-left to run queries directly against the Moss API route and inspect the returned documents and confidence scores without calling the LLM. | ||
|
|
||
| --- | ||
|
|
||
| ## How MOSS & CopilotKit Handoff Works | ||
|
|
||
| ### 1. The Hook (`app/use-moss-retrieval.ts`) | ||
| The `useMossRetrieval` hook registers the `searchKnowledgeBase` action with CopilotKit using `useCopilotAction`. | ||
|
|
||
| ```typescript | ||
| useCopilotAction({ | ||
| name: "searchKnowledgeBase", | ||
| description: "Searches the internal knowledge base to retrieve relevant context...", | ||
| parameters: [ | ||
| { | ||
| name: "query", | ||
| type: "string", | ||
| description: "The search query to match against documents.", | ||
| required: true, | ||
| }, | ||
| ], | ||
| handler: async ({ query }) => { | ||
| // 1. Triggered when the agent decides it needs knowledge. | ||
| // 2. We perform a client-side fetch to our secure server-side endpoint. | ||
| const response = await fetch(`/api/moss/query?query=${encodeURIComponent(query)}`); | ||
| const data = await response.json(); | ||
|
|
||
| // 3. Return results back to the agent for grounding. | ||
| return { | ||
| success: true, | ||
| documents: data.docs || [], | ||
| }; | ||
| } | ||
| }); | ||
| ``` | ||
|
|
||
| ### 2. The Secure Endpoint (`app/api/moss/query/route.ts`) | ||
| Queries are executed on the server via `@moss-dev/moss` to prevent exposing secret keys to the browser: | ||
|
|
||
| ```typescript | ||
| import { MossClient } from "@moss-dev/moss"; | ||
|
|
||
| const mossClient = new MossClient( | ||
| process.env.MOSS_PROJECT_ID, | ||
| process.env.MOSS_PROJECT_KEY | ||
| ); | ||
|
|
||
| export async function GET(request: Request) { | ||
| const { searchParams } = new URL(request.url); | ||
| const query = searchParams.get("query")!; | ||
| const indexName = process.env.MOSS_INDEX_NAME!; | ||
|
|
||
| const results = await mossClient.query(indexName, query, { topK: 3 }); | ||
| return NextResponse.json({ docs: results.docs }); | ||
| } | ||
| ``` |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,45 @@ | ||
| import { CopilotRuntime, OpenAIAdapter, copilotRuntimeNextJSAppRouterEndpoint } from "@copilotkit/runtime"; | ||
| import { NextRequest, NextResponse } from "next/server"; | ||
| import OpenAI from "openai"; | ||
|
|
||
| export const POST = async (req: NextRequest) => { | ||
| const apiKey = process.env.OPENAI_API_KEY; | ||
|
|
||
| if (!apiKey || apiKey === "your_openai_api_key") { | ||
| // If OpenAI API key is missing, return a helpful error indicating it must be set. | ||
| return new NextResponse( | ||
| JSON.stringify({ | ||
| error: "OPENAI_API_KEY is not configured. Please set it in your environment variables to allow CopilotKit to communicate with the LLM.", | ||
| }), | ||
| { | ||
| status: 400, | ||
| headers: { "content-type": "application/json" }, | ||
| } | ||
| ); | ||
| } | ||
|
|
||
| try { | ||
| const openai = new OpenAI({ apiKey }); | ||
| const serviceAdapter = new OpenAIAdapter({ openai }); | ||
| const runtime = new CopilotRuntime(); | ||
|
|
||
| const { handleRequest } = copilotRuntimeNextJSAppRouterEndpoint({ | ||
| runtime, | ||
| serviceAdapter, | ||
| endpoint: "/api/copilotkit", | ||
| }); | ||
|
|
||
| return handleRequest(req); | ||
| } catch (error: any) { | ||
| console.error("CopilotKit runtime initialization failed:", error); | ||
| return new NextResponse( | ||
| JSON.stringify({ | ||
| error: error.message || "Failed to handle CopilotKit runtime request", | ||
| }), | ||
| { | ||
| status: 500, | ||
| headers: { "content-type": "application/json" }, | ||
| } | ||
| ); | ||
| } | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,132 @@ | ||
| import { NextResponse } from "next/server"; | ||
| import { MossClient } from "@moss-dev/moss"; | ||
|
|
||
| // Retrieve environment variables | ||
| const projectId = process.env.MOSS_PROJECT_ID; | ||
| const projectKey = process.env.MOSS_PROJECT_KEY; | ||
|
|
||
| // Mock database for testing without API keys out-of-the-box | ||
| const MOCK_DOCUMENTS = [ | ||
| { | ||
| id: "refund-policy", | ||
| text: "Refund Policy: Customers can return any unused item in its original packaging within 30 days of purchase for a full refund. Return shipping costs are covered by the customer, and original shipping fees are non-refundable. Refunds are processed within 5-7 business days.", | ||
| metadata: { category: "policy" } | ||
| }, | ||
| { | ||
| id: "shipping-options", | ||
| text: "Shipping Options: We offer Standard Shipping (3-5 business days) for $5.99 (free on orders over $50) and Express Shipping (1-2 business days) for $14.99. We currently only ship within North America.", | ||
| metadata: { category: "shipping" } | ||
| }, | ||
| { | ||
| id: "office-hours-location", | ||
| text: "Office Info: Our corporate headquarters is located at 123 Innovation Way, Suite 400, San Francisco, CA 94107. We are open Monday through Friday, 9:00 AM to 6:00 PM PST. The office is closed on major US holidays.", | ||
| metadata: { category: "info" } | ||
| }, | ||
| { | ||
| id: "support-contact", | ||
| text: "Contact Support: Customer support is available 24/7. You can email us at support@usemoss.dev, call us toll-free at 1-800-555-0199, or open a live chat on our website during normal business hours.", | ||
| metadata: { category: "contact" } | ||
| }, | ||
| { | ||
| id: "what-is-moss", | ||
| text: "Moss is a real-time semantic search runtime for AI agents targeting sub-10ms query latency. It runs on-device using bundled embedding models, requiring no external API calls for local queries. The cloud layer handles project management and index distribution.", | ||
| metadata: { category: "moss-info" } | ||
| } | ||
| ]; | ||
|
|
||
| let mossClient: MossClient | null = null; | ||
| const isMockMode = !projectId || !projectKey || projectId === "your_moss_project_id" || projectKey === "your_moss_project_key"; | ||
|
|
||
| if (!isMockMode) { | ||
| try { | ||
| mossClient = new MossClient(projectId!, projectKey!); | ||
| console.log("MossClient successfully initialized on server."); | ||
| } catch (error) { | ||
| console.error("Failed to initialize MossClient:", error); | ||
| } | ||
| } else { | ||
| console.warn("Moss credentials not configured or set to placeholder values. Running in MOCK mode."); | ||
| } | ||
|
|
||
| export async function GET(request: Request) { | ||
| try { | ||
| const { searchParams } = new URL(request.url); | ||
| const query = (searchParams.get("query") ?? "").trim(); | ||
| const indexName = searchParams.get("indexName") || process.env.MOSS_INDEX_NAME; | ||
| const topKRaw = searchParams.get("topK"); | ||
| const parsedTopK = Number.parseInt(topKRaw ?? "3", 10); | ||
| const topK = Number.isFinite(parsedTopK) | ||
| ? Math.min(Math.max(parsedTopK, 1), 20) | ||
| : 3; | ||
|
|
||
| if (!query) { | ||
| return NextResponse.json({ error: "Missing query parameter" }, { status: 400 }); | ||
| } | ||
|
|
||
| // MOCK MODE FALLBACK | ||
| if (isMockMode || !mossClient) { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. BLOCKING This falls back to sample data even when real Moss credentials were supplied but client initialization failed: if (isMockMode || !mossClient) {That can make a misconfigured production demo answer with the built-in refund/contact mock docs instead of failing visibly. Only use mock mode when credentials are actually absent/placeholders; if |
||
| console.log(`[MOCK MODE] Searching (topK=${topK})`); | ||
|
|
||
| // Perform a simple case-insensitive keyword match score simulation | ||
| const queryWords = query.toLowerCase().split(/\s+/); | ||
| const results = MOCK_DOCUMENTS.map(doc => { | ||
| let matches = 0; | ||
| const textLower = doc.text.toLowerCase(); | ||
|
|
||
| queryWords.forEach(word => { | ||
| if (word.length > 2 && textLower.includes(word)) { | ||
| matches++; | ||
| } | ||
| }); | ||
|
|
||
| // Calculate a simulated score | ||
| const score = matches > 0 ? 0.3 + (matches * 0.15) : 0.05; | ||
|
|
||
| return { | ||
| id: doc.id, | ||
| text: doc.text, | ||
| score: Math.min(score, 0.99), | ||
| metadata: doc.metadata | ||
| }; | ||
| }) | ||
| .filter(doc => doc.score > 0.1) // Only return matches above threshold | ||
| .sort((a, b) => b.score - a.score) | ||
| .slice(0, topK); | ||
|
|
||
| return NextResponse.json({ | ||
| docs: results, | ||
| mode: "mock", | ||
| warning: isMockMode | ||
| ? "Running in mock mode. Add MOSS_PROJECT_ID and MOSS_PROJECT_KEY to your environment variables to query real indexes." | ||
| : "Moss credentials were provided, but MossClient failed to initialize. Check server logs and native bindings." | ||
| }); | ||
| } | ||
|
|
||
| // REAL MOSS QUERY | ||
| if (!indexName || indexName === "your_moss_index_name") { | ||
| return NextResponse.json({ | ||
| error: "Moss index name is not configured. Please set MOSS_INDEX_NAME in your environment." | ||
| }, { status: 400 }); | ||
| } | ||
|
|
||
| console.log(`[REAL MODE] Querying Moss index "${indexName}" (topK=${topK})`); | ||
| const results = await mossClient.query(indexName, query, { topK }); | ||
|
|
||
| return NextResponse.json({ | ||
| docs: results.docs.map(doc => ({ | ||
| id: doc.id, | ||
| text: doc.text, | ||
| score: doc.score, | ||
| metadata: doc.metadata, | ||
| })), | ||
| timeTakenInMs: results.timeTakenInMs, | ||
| mode: "real" | ||
| }); | ||
|
|
||
| } catch (error: any) { | ||
| console.error("API route query error:", error); | ||
| return NextResponse.json({ | ||
| error: error.message || "Internal server error occurred while querying knowledge base." | ||
| }, { status: 500 }); | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
BLOCKING This lets a browser request choose which secret-backed Moss index to query:
Anyone who can reach
/api/moss/querycan query any index in the configured Moss project if they know or guess its name. Keep the index selection on the server side, or validate it against a server-side allowlist after authenticating the user, e.g.const indexName = process.env.MOSS_INDEX_NAME;.