Giter VIP home page Giter VIP logo

wishful-search's Introduction


WishfulSearch
WishfulSearch

Multi-model natural language search for any JSON.

Twitter Follow License

Key FeaturesInstallationUsageDemo: Search MoviesHow it works

output2

WishfulSearch is a natural language search module for JSON arrays. Take any JSON array you have (notifications, movies, flights, people) and filter it with complex questions. WishfulSearch takes care of the prompting, database management, object-to-relational conversion and query formatting.

This repo is the work of one overworked dev, and meant to be for educational purposes. Use at your own risk!

If you aren't new here, check out autosearch!

Key Features

  • AI Quickstart - just bring an object

    • Generate everything you need to use the library from a single, untyped JS object. Schema, functions, all of it.
  • Database Batteries Included

    • WishfulSearch comes included with a performant sqlite database bundled for use, managed by the module.
  • Server and client-side

  • Automated few-shot generation

    • Use a smarter model to generate few-shot examples from a few questions, retemplate and insert into a prompt of a local model for instantly better results.
  • Multi-model

    • GPT, Claude, Mistral adapters (OpenAI, Anthropic and Ollama) are provided, with specific-model template generation from the same input with advanced things like model-resume. Feel free to swap models midway through a conversation!
  • Single production dependency

    • The only prod dependency is sql.js, so you're not dragging along Guy Fieri if you don't want to.
  • Exposed prompts - do your own thing

    • Use the entire functionality, or don't. Most key functions are exposed, including those for prompt generation. Use as you wish.

Better Search

  • Search history
    • The LLM is appropriately fed past queries, so users can ask contextual questions ('What trains go to paris?' followed by 'any leaving at 10 am') and have auto-merged filters.
  • Automated Dynamic Enums
    • The structured DDL format used internally (and generated by the AI Quickstart) contains the option for you to propose static examples for each column, to make the contents of a field clear to the model. WishfulSearch can also dynamically generate example values (with type detection) on each insert, so this is done with no effort. It can also pick the most frequent values in a column, or find the range and pass that on for token savings.
  • Safer search
    • While running auto-generated queries can never be properly safe. WishfulSearch implements a few filters to sanitize the output, as well only having the LLM generate partial queries to try and improve safety. Ideally this is used in cases where having the entire db exposed to the user is not a security risk.

Installation

Server:

npm i wishful-search

Client:

Just get the bundled wishfulsearch.js, or compile a smaller one yourself from source. More instructions coming if this ends up a common use-case.

Usage

Selecting your model

You'll need an OpenAI or Anthropic instance (unless you're using Ollama and Mistral, in which case you just need to pull in an adapter).

import OpenAI from 'openai';
const openai = new OpenAI();

import Anthropic from '@anthropic-ai/sdk';
const anthropic = new Anthropic();

const GPTLLMAdapter = LLMAdapters.getOpenAIAdapter(openai, {
  model: 'gpt-4',
});

const ClaudeLLMAdapter = LLMAdapters.getClaudeAdapter(
  Anthropic.HUMAN_PROMPT,
  Anthropic.AI_PROMPT,
  anthropic,
  {
    model: 'claude-2',
  },
);

const MistralLLMAdapter = LLMAdapters.getMistralAdapter({
  model: 'mistral',
  temperature: 0.1,
});

You can now use any of these for the Quickstart or Search functions.

AI Quickstart

WishfulSearch needs three things from you:

  1. Structured DDL: This is just an object that encodes the column names, types, examples, description and so on, in the SQL tables that are created.
  2. ObjectToRelational Function: This is the function that takes a (nested) object and converts it to flat rows that can be inserted into tables.
  3. Primary table and column: Just the name of the main table (usually the first one) and the column inside the main table to be used as the retrieval id.
  4. (Optional) Few-shot learning: This is the most useful thing you can do to improve performance. Look through the examples, or generate your own at runtime with a function call.
  5. (Optional) sql.js wasm file: If you use this client-side, you'll need to provide a URL or a file that sql.js can use to do it's thing. You can look here for more information.

All of the required pieces can be generated by the AI Quickstart:

import OpenAI from 'openai';
const openai = new OpenAI();
import { autoAnalyzeObject, LLMAdapters } from 'wishful-search';
const GPTLLMAdapter = LLMAdapters.getOpenAIAdapter(openai, {
  model: 'gpt-4',
});
const results = await autoAnalyzeObject(
  movies[0],
  GPTLLMAdapter.callLLM,
  '~/tmp',
);

GPT-4 and Claude-2 perform similarly and are recommended.

results will contain the same info as an analysis_{date}.md file placed in the directory of your choice. The file should contain instructions, along with the structured DDL and the ObjectToRelational function. Make sure to read the code - if your objects are complex it might need some tweaking - before you use it!

Smart models can get you 99% of the way there in most cases, but some common things to look out for:

  1. No dynamic enum settings in the structured DDL - you may not need these, but GPT-4 finds it hard to recommend any.
  2. No default values in case your objects are sometimes missing fields. We generate the entire thing from a single object, so the model simply doesn't know which fields are missing or optional. Providing your own typespec as a parameter should fix this.

Create

Once you have these, you can create a new WishfulSearch instance:

const wishfulSearchEngine = await WishfulSearchEngine.create(
    'movies', // Name for your search instance, for labelling
    MOVIES_DDL, // Structured DDL
    {
      table: 'Movies', // Primary table
      column: 'id', // Primary id column
    },
    movieToRows, // Object to relational function
    {
      enableTodaysDate: true, // Inform the model about the date
      fewShotLearning: [], // Few-shot examples
    },
    GPT4LLMAdapter.callLLM, // LLM calling function
    (movie: Movie) => movie.id, // Object to id function
    true, // Save question history?
    true, // Enable dynamic enums on insert?
    true // Sort dynamic enums by frequency? Light performance penalty on insert but better searches and token savings
  	undefined // sql.js wasm URL
  );

Insert

You can insert your objects into the instance by simply passing them in:

const errors = wishfulSearchEngine.insert(TEST_MOVIES);

In this case, any insertion errors are passed back to you as an array. If you enable the second parameter, all insertion is rolled back when an error is encountered, and an exception is thrown.

AI Few-shot generation

Use larger models to teach smaller models how to behave in a few lines! Import a smarter model adapter (see above) and pass it into autoGenerateFewShot.

await wishfulSearchEngine.autoGenerateFewShot(
  SmarterLLMAdapter.callLLM,
  [
    {
      question: 'something romantic?',
    },
    {
      question: 'from the 80s?',
    },
    {
      question: 'okay sci-fi instead.',
      clearHistory: true,
    },
  ],
  false, // Should we remove questions that don't get results?
  false, // throw an error on invalid questions
  true, // Be verbose
);

clearHistory is recommended in between, to teach the model when to reset the filters based on user questions.

The function also returns the same format of question-answers used to create the instance, so you can save or edit it - or mix and match with generations from different models!

Search

This is the easy bit.

const results = (await wishfulSearchEngine.search(
  'Something romantic but not very short from the 80s',
)) as Movie[];

Autosearch

Autosearch.Demo.mov

Autosearch adds analysis and looping to iteratively improve results. Standard search is blind - while the LLM is made aware of the contents of the tables, it doesn't know how it did.

Autosearch provides this information back to the LLM, along with results of past rounds, to hypothesize about what the user wants, and what is being delivered. For more complex searches - where you have the tokens and time - this can greatly improve results. See /tests/movies.autosearch.ts for an example.

The function documentation explains each parameter for autosearch in more detail.

Example: Movies

The demo shows filters in the Kaggle movies dataset.

To use:

  1. Download movies_metadata.csv.
  2. Place it in tests/data.
  3. Run tests/movies.run.ts with npx ts-node tests/movies.run.ts.

You can uncomment the different adapters for GPT, Claude, Mistral. Comment and uncomment the few-shot generation to see how behavior changes. Have fun!

How it works

A few things happen in order to perform a search:

  1. The JSON objects are converted to relational tables with foreign-keys and stored in the embedded sqlite db.
  2. Dynamic enum values are generated to help inform the LLM about the contents of each relevant column.
  3. User queries are translated into complete context, including table structure, contents, past questions, and passed to the LLM to generate a SQL query that retrieves the relevant ids.
  4. Results of the query are (optionally) used to retrieve the relevant object and returned to the caller.

Authenticating with APIs

The simplest and safest method is going to be using export OPENAI_API_KEY=XXXX or export ANTHROPIC_API_KEY=XXXX before running your code. You can also instantiate your openai and anthropic objects with the API keys inside, as per the guides linked for each sdk above.

Other utilities

  • llm-adapters.ts exposes the templating functions for each model.
  • WishfulSearchEngine exposes the following additional functions:
    • generateSearchMessages returns (in OpenAI message format) the messages that go to the LLM to generate the query.
    • searchWithPartialQuery can be used to perform the search with the query response you get from the LLM.

Where are the prompts?

I tend to read repos prompt first. In this case, most of the complexity is in formatting the output and injecting things at the right time, but if you'd like to do the same, here are the prompts for AI Quickstart and for Search.

TODO

  1. Tests: More robust tests are needed before production usage. Unfortunately that's outside my scope at the moment, but I'll update the repo if I get around to it! Help would be appreciated.
  2. Client-side testing: The client-side bundle has been tested in a limited fashion. It's hard to keep running all the toolchains without automated testing for now. If you run into any issues, let me know.

wishful-search's People

Contributors

albertsyh avatar as-greywing avatar hrishioa avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

wishful-search's Issues

Ollama/Mistral Fixes and Improvements

I wasn't able to get the movies.run.ts example to work as is and had to change "template:" to "prompt:" here:
https://github.com/hrishioa/wishful-search/blob/master/src/ollama.ts#L24

I was also experiencing UND_ERR_HEADERS_TIMEOUT errors because the LLM was taking longer to respond than the fetch default of 30 seconds so I made the following changes:
Added the "undici" dependency to package.json and ran npm install:

"dependencies": {
    "@anthropic-ai/sdk": "^0.16.1",
    "@azure/openai": "^1.0.0-beta.9",
    "openai": "^4.24.7",
    "sql.js": "^1.8.0",
    "undici": "^6.19.0",
  },

Added the following to the beginning of ollama.ts:
import { fetch, Agent } from 'undici';
And replaced:

// Using fetch to initiate a POST request that will return an SSE stream
  const response = await fetch(`http://127.0.0.1:${port}/api/generate`, {
    method: 'POST',
    headers: headers,
    body: requestBody,
  });

With:

// Using fetch to initiate a POST request that will return an SSE stream
  const response = await fetch(`http://127.0.0.1:${port}/api/generate`, {
    method: 'POST',
    headers: headers,
    body: requestBody,
    dispatcher: new Agent({
      headersTimeout: 10 * 60e3,  // 10 minutes
      bodyTimeout: 10 * 60e3,     // 10 minutes
    }),
  });

I also found that when the response from the LLM contained a curly brace, the json parsing would fail so I replaced the following in ollama.ts:

      // Attempt to parse all complete JSON objects in the buffer
      while (true) {
        const openingBraceIndex = textBuffer.indexOf('{');
        const closingBraceIndex = textBuffer.indexOf('}');

        // Check if we have a complete object
        if (openingBraceIndex !== -1 && closingBraceIndex !== -1) {
          // Extract and parse the JSON object
          const jsonString = textBuffer.slice(
            openingBraceIndex,
            closingBraceIndex + 1,
          );

With (there may be better ways to achieve this):

      const regex = /{.*?"done":(?:true|false).*?}/g;
      const jsonStrings = textBuffer.match(regex);
      if (jsonStrings == null) {
        continue;
      } else if (jsonStrings.length == 0) {
        continue;
      }

      // Attempt to parse all complete JSON objects in the buffer
      for (let i = 0; i < jsonStrings.length; i++) {
        const currentJsonString = jsonStrings[i];
	if (currentJsonString == null) {
            break;
        }
        const closingBraceIndex = currentJsonString.length;

        // Check if we have a complete object
        if (closingBraceIndex !== -1) {
          const jsonString = currentJsonString;

Issues running `movies.run.ts`

Thanks for the great work @hrishioa!

I am running into some issues trying to run the example movies.run.ts script.

At first, I ran into the following issue Property 'queryPrefix' is missing in type.

This was an easy fix as I just needed to add "queryPrefix": "" to MOVIES_FEW_SHOT in movies-data.ts:

{
    "queryPrefix": "",
    "question": "something romantic?",
    "partialQuery": "WHERE id IN (\n    SELECT movie_id FROM Genres WHERE genre_name = 'Romance'\n)"
  },

Not sure if an empty queryPrefix is correct though.

Now when I run npx ts-node ./tests/movies.run.ts, I get the following error:

What are you looking for? I want to watch a good comedy movie

Query:  SELECT id FROM Movies WHERE genre_name LIKE '%Comedy%' AND vote_average >= 7.0 ORDER BY popularity DESC, vote_average DESC LIMIT 10
Error in query  WHERE genre_name LIKE '%Comedy%' AND vote_average >= 7.0 ORDER BY popularity DESC, vote_average DESC LIMIT 10  - reflecting and fixing error  Error: no such column: genre_name
    at e.handleError (/repos/wishful-search/node_modules/sql.js/dist/sql-wasm.js:90:245)
    at e.exec (/repos/wishful-search/node_modules/sql.js/dist/sql-wasm.js:87:501)
    at LLMSearcheableDatabase.rawQuery (/repos/wishful-search/src/db.ts:155:28)
    at WishfulSearchEngine.searchWithPartialQuery (/repos/wishful-search/src/search-engine.ts:345:29)
    at WishfulSearchEngine.<anonymous> (/repos/wishful-search/src/search-engine.ts:675:28)
    at Generator.next (<anonymous>)
    at fulfilled (/repos/wishful-search/src/search-engine.ts:5:58)
    at processTicksAndRejections (node:internal/process/task_queues:95:5)

Seems like the database just doesn't have the right columns. Am I missing anything? Does an empty queryPrefix break it?

Open AI API Key

Hi,
Apologies for the basic question but I'm not too familiar with typescript. Where do I put my openAI key in the index.ts file?

Would love to try this tool out but can't even get it running with the demo movies script because of this.

Allow for sql-wasm errors to propagate

When creating a new wishful search instance with auto-generated code, wasm-sql might error. However, the errors are not propagating correctly:

db.js

console.log({strDDL})
{
  strDDL: 'CREATE TABLE IF NOT EXISTS Inventory (\n' +
    'id INTEGER, --Auto-incrementing primary key\n' +
    "category VARCHAR(50), --Main category of the inventory item: 'Dienstleistungen' or 'Baumaterialien'\n" +
    'quantity INTEGER, --Quantity of the inventory item\n' +
    "sub_category VARCHAR(10), --Sub-category of the inventory item: 'm2', 'kg', or 'Std'\n" +
    'description VARCHAR(100), --Description of the inventory item\n' +
    ');'
}

db.run(strDDL);

/search/node_modules/sql.js/dist/sql-wasm.js:90
(Object.values(this.Za).forEach(function(g){g.free()}),Object.values(this.Na).forEach(ua),this.Na={},this.handleError(y(this.db)),wa("/"+this.filename),this.db=null)};e.prototype.handleError=funct
ion(g){if(0===g)return null;g=Bc(this.db);throw Error(g);};e.prototype.getRowsModified=function(){return F(this.db)};e.prototype.create_function=function(g,l){Object.prototype.hasOwnProperty.call(
this.Na,g)&&(ua(this.Na[g]),delete this.Na[g]);var n=xa(function(t,w,z){w=b(w,z);try{var N=l.apply(null,w)}catch(L){Aa(t,

                                                ^

Error: near ")": syntax error
    at e.handleError

autoAnalyzeObject throws error "No response from LLM while generating typespec!"

Following the example on the readme. My code looks like:

const openai = new OpenAI({
  apiKey: process.env.OPENAI_API_KEY,
});

const GPTLLMAdapter = LLMAdapters.getOpenAIAdapter(openai, {
  model: "gpt-4",
});

const results = await autoAnalyzeObject(
  data[0],
  GPTLLMAdapter.callLLM,
  "~/tmp",
);

I am receiving error error: No response from LLM while generating typespec! which is coming from file auto-analyze.js.

When I look at the code: https://github.com/hrishioa/wishful-search/blob/master/src/auto-analyze.ts#L190-L204 I can see:

  if (!typespec) {
    console.log('Generating typespec...');

    if (!typespec)
      throw new Error('No response from LLM while generating typespec!');

    const extractedTypeSpecs = typespec.match(/```typescript([\s\S]*?)```/g);

    if (extractedTypeSpecs?.length) {
      typespec = extractedTypeSpecs[0]
        .replace(/```typescript/g, '')
        .replace(/```/g, '')
        .trim();
    }
  }

This must be a bug, seems like we are forgetting to generate the typespec if it doesn't exist?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.