Part 2: Enhancing the Motoku LLM Retrieval System with OpenAI Embeddings and Prompt-based Retrieval

Community Article Published June 28, 2024

image/webp

In Part 1, we established a foundational Motoko LLM retrieval system using Internet Computer Protocol (ICP), Motoko, and Node.js. Now, we will enhance this system by integrating OpenAI embeddings and adding a feature to retrieve embeddings based on a prompt. This involves utilizing OpenAI's powerful embeddings API to generate embeddings for text data before storing it in the Motoko canister and enabling prompt-based retrieval.

Prerequisites

Before proceeding, ensure you have completed the setup from Part 1 and have the following additional tools and knowledge:

  • An OpenAI API key.
  • Familiarity with OpenAI's embeddings API.
  • Updated Node.js environment with axios installed for making HTTP requests.

Step 4: Integrating OpenAI Embeddings and Prompt-based Retrieval

4.1 Install axios

First, install axios to enable HTTP requests to the OpenAI API:

npm install axios

4.2 Update Environment Configuration

Add your OpenAI API key to the .env file:

OPENAI_API_KEY=<your-openai-api-key>

4.3 Modify the Node.js Server Script

Update server.js to integrate with the OpenAI embeddings API and handle prompt-based retrieval. Replace the existing content with the following:

const express = require('express');
const bodyParser = require('body-parser');
const axios = require('axios');
const { HttpAgent, Actor } = require('@dfinity/agent');
const { idlFactory } = require('./idl/embedding_store.did.js');
require('dotenv').config();

const app = express();
const port = 3000;

app.use(bodyParser.json());

const canisterId = process.env.CANISTER_ID;
const host = process.env.HOST;
const openaiApiKey = process.env.OPENAI_API_KEY;

// Initialize the agent
const agent = new HttpAgent({ host });
agent.fetchRootKey(); // Necessary for local development

// Create an actor instance
const embeddingStore = Actor.createActor(idlFactory, {
    agent,
    canisterId,
});

// Helper function to convert BigInt to a string for JSON serialization
const serializeBigInt = (obj) => {
    if (typeof obj === 'bigint') {
        return obj.toString();
    } else if (Array.isArray(obj)) {
        return obj.map(serializeBigInt);
    } else if (typeof obj === 'object' && obj !== null) {
        return Object.fromEntries(
            Object.entries(obj).map(([k, v]) => [k, serializeBigInt(v)])
        );
    }
    return obj;
};

const getOpenAIEmbedding = async (text) => {
    try {
        const response = await axios.post(
            'https://api.openai.com/v1/embeddings',
            { input: text, model: 'text-embedding-ada-002' },
            {
                headers: {
                    'Authorization': `Bearer ${openaiApiKey}`,
                    'Content-Type': 'application/json'
                }
            }
        );
        return response.data.data[0].embedding;
    } catch (error) {
        console.error('Error fetching embedding from OpenAI:', error);
        throw new Error('Failed to fetch embedding from OpenAI');
    }
};

const cosineSimilarity = (vecA, vecB) => {
    const dotProduct = vecA.reduce((sum, a, idx) => sum + a * vecB[idx], 0);
    const normA = Math.sqrt(vecA.reduce((sum, a) => sum + a * a, 0));
    const normB = Math.sqrt(vecB.reduce((sum, b) => sum + b * b, 0));
    return dotProduct / (normA * normB);
};

app.post('/storeEmbedding', async (req, res) => {
    const { key, text } = req.body;
    try {
        if (key !== process.env.SECRET_KEY) {
            throw new Error('Invalid key');
        }
        // Get embedding from OpenAI
        const embedding = await getOpenAIEmbedding(text);
        await embeddingStore.storeEmbedding(key, text, embedding);
        res.status(200).send('Embedding stored successfully.');
    } catch (error) {
        res.status(500).send(`Error: ${error.message}`);
    }
});

app.post('/retrieveEmbeddings', async (req, res) => {
    const { prompt } = req.body;
    try {
        // Get prompt embedding from OpenAI
        const promptEmbedding = await getOpenAIEmbedding(prompt);

        // Retrieve stored embeddings
        const embeddings = await embeddingStore.getEmbeddings();

        // Calculate similarities
        const results = embeddings.map((embedding) => ({
            text: embedding.text,
            similarity: cosineSimilarity(promptEmbedding, embedding.embedding),
        }));

        // Sort by similarity
        results.sort((a, b) => b.similarity - a.similarity);

        res.status(200).json(serializeBigInt(results));
    } catch (error) {
        res.status(500).send(`Error: ${error.message}`);
    }
});

app.listen(port, () => {
    console.log(`Server is running on http://localhost:${port}`);
});

4.4 Test the Updated System

4.4.1 Storing an Embedding

Store an embedding using the updated endpoint:

curl -X POST http://localhost:3000/storeEmbedding \
     -H "Content-Type: application/json" \
     -d '{"key": "8529741360", "text": "example text"}'

This will generate an embedding for the provided text using OpenAI and store it in the Motoko canister.

4.4.2 Retrieving Embeddings Based on a Prompt

Retrieve embeddings based on a prompt:

curl -X POST http://localhost:3000/retrieveEmbeddings \
     -H "Content-Type: application/json" \
     -d '{"prompt": "example prompt"}'

This will generate an embedding for the prompt using OpenAI, calculate the cosine similarity between the prompt embedding and stored embeddings, and return the results sorted by similarity.

Conclusion

In this enhanced version, we integrated OpenAI embeddings to improve the retrieval system's performance and added prompt-based retrieval functionality. The Node.js server now interacts with the OpenAI API to generate embeddings for the input text before storing them in the Motoko canister, and it can retrieve and rank stored embeddings based on the similarity to a given prompt. This setup leverages the decentralized capabilities of ICP and the advanced natural language processing capabilities of OpenAI, providing a robust solution for LLM retrieval.

As a next step, you could explore implementing more advanced search and filtering capabilities, integrating user authentication, or developing a frontend interface for your system.