Dewy est une base de connaissances OSS conçue pour rationaliser la façon dont les développeurs stockent, organisent et récupèrent les informations. Sa flexibilité et sa facilité d'utilisation en font un excellent choix pour les développeurs souhaitant créer des applications basées sur la connaissance.
LangChain.js , quant à lui, est un framework puissant qui permet aux développeurs d'intégrer de manière transparente les LLM dans leurs applications. En combinant la gestion structurée des connaissances de Dewy avec les capacités LLM de LangChain.js, les développeurs peuvent créer des systèmes de questions-réponses sophistiqués capables de comprendre et de traiter des requêtes complexes, offrant des réponses précises et contextuellement pertinentes.
mkdir dewy_qa cd dewy_qa
Une fois le répertoire configuré, vous pouvez installer TypeScript et initialiser le projet : npm init -y npm i typescript --save-dev npx tsc --init
En fonction de votre environnement, vous devrez peut-être apporter quelques modifications à votre configuration TypeScript. Assurez-vous que votre tsconfig.json
ressemble à ceci :
{ "compilerOptions": { "target": "ES6", "module": "CommonJS", "moduleResolution": "node", "declaration": true, "outDir": "./dist", "esModuleInterop": true, "strict": true, }
Vous êtes maintenant prêt à créer l'application CLI. Pour éviter que le code ne devienne trop compliqué, organisez-le dans plusieurs répertoires, avec la disposition suivante dewy_qa/ ├── commands/ │ └── ... ├── utils/ │ └── ... ├── index.ts ├── package.json └── tsconfig.ts
Chaque commande sera implémentée dans le répertoire commands
et le code partagé ira dans le répertoire utils
. Le point d'entrée de l'application CLI est le fichier index.ts
.
Commencez par une simple version "hello world" d' index.ts
- vous commencerez à la remplir dans la section suivante
#!/usr/bin/env ts-node-script console.log("hello world");
Pour vérifier que l'environnement est correctement configuré, essayez d'exécuter la commande suivante : vous devriez voir "hello world" imprimé dans la console : npx ts-node index.ts
Plutôt que de taper cette très longue commande à chaque fois, créons une entrée dans package.json
pour la commande. Cela nous aidera à nous rappeler comment appeler la CLI et facilitera son installation en tant que commande :
{ ... "bin": { "dewy_qa": "./index.ts" } ... }
Vous pouvez maintenant exécuter votre script avec npm exec dewy_qa
ou npm link
le package et l'exécuter simplement comme dewy_qa
Chargez des documents en configurant le client Dewy. La première étape consiste à ajouter quelques dépendances au projet. La première est dewy-ts
, la bibliothèque client de Dewy. Le second est commander
, qui nous aidera à créer une application CLI avec une analyse des arguments, des sous-commandes, etc. Enfin, chalk
rend les invites plus colorées.
npm install dewy-ts commander chalk
Ensuite, implémentez la logique de la commande de chargement. Vous ferez cela dans un fichier séparé nommé commands/load.ts
. Ce fichier implémente une fonction nommée load
, qui attend une URL et quelques options supplémentaires - cela sera connecté à la CLI dans une section ultérieure.
Dewy rend le chargement des documents très simple : il suffit de configurer le client et d'appeler addDocument
avec l'URL du fichier que vous souhaitez charger. Dewy s'occupe d'extraire le contenu du PDF, de le diviser en morceaux de la bonne taille pour l'envoyer à un LLM et de les indexer pour une recherche sémantique.
import { Dewy } from 'dewy-ts'; import { success, error } from '../utils/colors'; export async function load(url: string, options: { collection: string, dewy_endpoint: string }): Promise<void> { console.log(success(`Loading ${url} into collection: ${options.collection}`)); try { const dewy = new Dewy({ BASE: options.dewy_endpoint }) const result = await dewy.kb.addDocument({ collection: options.collection, url }); console.log(success(`File loaded successfully`)); console.log(JSON.stringify(result, null, 2)); } catch (err: any) { console.error(error(`Failed to load file: ${err.message}`)); } }
Vous avez peut-être remarqué que certaines fonctions ont été importées depuis ../utils/colors
. Ce fichier configure simplement quelques assistants pour colorer la sortie de la console - placez-le dans utils
afin qu'il puisse être utilisé ailleurs :
import chalk from 'chalk'; export const success = (message: string) => chalk.green(message); export const info = (message: string) => chalk.blue(message); export const error = (message: string) => chalk.red(message);
Pour commencer, installez quelques packages supplémentaires - langchain
et openai
pour utiliser l'API OpenAI comme LLM :
npm install dewy-langchain langchain @langchain/openai openai
La première chose à configurer est Dewy (comme avant) et un LLM. Une différence par rapport à avant est que dewy
est utilisé pour construire un DewyRetriever
: il s'agit d'un type spécial utilisé par LangChain pour récupérer des informations dans le cadre d'une chaîne. Vous verrez comment le retriever est utilisé dans une minute.
const model = new ChatOpenAI({ openAIApiKey: options.openai_api_key, }); const dewy = new Dewy({ BASE: options.dewy_endpoint }) const retriever = new DewyRetriever({ dewy, collection });
Il s'agit d'un modèle de chaîne qui indique au LLM comment il doit se comporter, avec des espaces réservés pour un contexte supplémentaire qui seront fournis lors de la création de la « chaîne ». Dans ce cas, le LLM est chargé de répondre à la question, mais uniquement en utilisant les informations fournies. Cela réduit la tendance du modèle à « halluciner » ou à inventer une réponse plausible mais fausse. Les valeurs du context
et question
sont fournies à l'étape suivante :
const prompt = PromptTemplate.fromTemplate(`Answer the question based only on the following context: {context} Question: {question}`);
Utilisez un RunnableSequence
pour créer une chaîne LCEL. Cette chaîne décrit comment générer les valeurs context
et question
: le contexte est généré à l'aide du récupérateur créé précédemment et la question est générée en passant par l'entrée de l'étape. Les résultats récupérés par Dewy sont formatés sous forme de chaîne en les redirigeant vers la fonction formatDocumentsAsString
.
DewyRetriever
, les affecte au context
et attribue la valeur d'entrée de la chaîne à question
.context
et question
. const chain = RunnableSequence.from([ { context: retriever.pipe(formatDocumentsAsString), question: new RunnablePassthrough(), }, prompt, model, new StringOutputParser(), ]);
Maintenant que la chaîne a été construite, exécutez-la et affichez les résultats sur la console. Comme vous le verrez, question
est un argument d'entrée fourni par l'appelant de la fonction.
L'exécution de la chaîne à l'aide chain.streamLog()
vous permet de voir chaque morceau de réponse tel qu'il est renvoyé par le LLM. La boucle du gestionnaire de flux est en quelque sorte moche, mais elle filtre simplement les résultats du flux appropriés et les écrit dans STDOUT
(en utilisant console.log
, elle aurait ajouté des nouvelles lignes après chaque morceau).
const stream = await chain.streamLog(question); // Write chunks of the response to STDOUT as they're received console.log("Answer:"); for await (const chunk of stream) { if (chunk.ops?.length > 0 && chunk.ops[0].op === "add") { const addOp = chunk.ops[0]; if ( addOp.path.startsWith("/logs/ChatOpenAI") && typeof addOp.value === "string" && addOp.value.length ) { process.stdout.write(addOp.value); } } }
Maintenant que vous avez vu tous les éléments, vous êtes prêt à créer la commande query
. Cela devrait ressembler à la commande load
d'avant, avec quelques importations supplémentaires.
import { StringOutputParser } from "@langchain/core/output_parsers"; import { PromptTemplate } from "@langchain/core/prompts"; import { formatDocumentsAsString } from "langchain/util/document"; import { RunnablePassthrough, RunnableSequence } from "@langchain/core/runnables"; import { ChatOpenAI } from "@langchain/openai"; import { Dewy } from 'dewy-ts'; import { DewyRetriever } from 'dewy-langchain'; import { success, error } from '../utils/colors'; export async function query(question: string, options: { collection: string, dewy_endpoint: string, openai_api_key: string }): Promise<void> { console.log(success(`Querying ${options.collection} collection for: "${question}"`)); try { const model = new ChatOpenAI({ openAIApiKey: options.openai_api_key, }); const dewy = new Dewy({ BASE: options.dewy_endpoint }) const retriever = new DewyRetriever({ dewy, collection: options.collection }); const prompt = PromptTemplate.fromTemplate(`Answer the question based only on the following context: {context} Question: {question}`); const chain = RunnableSequence.from([ { context: retriever.pipe(formatDocumentsAsString), question: new RunnablePassthrough(), }, prompt, model, new StringOutputParser(), ]); const stream = await chain.streamLog(question); // Write chunks of the response to STDOUT as they're received console.log("Answer:"); for await (const chunk of stream) { if (chunk.ops?.length > 0 && chunk.ops[0].op === "add") { const addOp = chunk.ops[0]; if ( addOp.path.startsWith("/logs/ChatOpenAI") && typeof addOp.value === "string" && addOp.value.length ) { process.stdout.write(addOp.value); } } } } catch (err: any) { console.error(error(`Failed to query: ${err.message}`)); } }
Avec Dewy et LangChain.js intégrés, l'étape suivante consiste à créer l'interface CLI. Utilisez une bibliothèque telle que commander
pour créer une interface de ligne de commande conviviale qui prend en charge les commandes permettant de charger des documents dans Dewy et d'interroger la base de connaissances à l'aide de LangChain.js.
Tout d’abord, réécrivez index.ts
pour créer les sous-commandes load
et query
. L'argument --collection
détermine dans quelle collection Dewy le document doit être chargé (Dewy vous permet d'organiser les documents en différentes collections, similaires aux dossiers de fichiers). L'argument --dewy-endpoint
vous permet de spécifier comment vous connecter à Dewy - par défaut, une instance exécutée localement sur le port 8000
est supposée. Enfin, l'argument --openai_api_key
(qui est par défaut une variable d'environnement) configure l'API OpenAI :
#!/usr/bin/env ts-node-script import { Command } from 'commander'; import { load } from './commands/load'; import { query } from './commands/query'; const program = new Command(); program.name('dewy-qa').description('CLI tool for interacting with a knowledge base API').version('1.0.0'); const defaultOpenAIKey = process.env.OPENAI_API_KEY; program .command('load') .description("Load documents into Dewy from a URL") .option('--collection <collection>', 'Specify the collection name', 'main') .option('--dewy-endpoint <endpoint>', 'Specify the collection name', '//localhost:8000') .argument('<url>', 'URL to load into the knowledge base') .action(load); program .command('query') .description('Ask questions using an LLM and the loaded documents for answers') .option('--collection <collection>', 'Specify the collection name', 'main') .option('--dewy-endpoint <endpoint>', 'Specify the collection name', '//localhost:8000') .option('--openai-api-key <key>', 'Specify the collection name', defaultOpenAIKey) .argument('<question>', 'Question to ask the knowledge base') .action(query); program.parse(process.argv);
OK, tout est terminé – n'était-ce pas facile ? Vous pouvez l'essayer en exécutant la commande : dewy_qa load //arxiv.org/pdf/2009.08553.pdf
Vous devriez voir quelque chose comme Loading //arxiv.org/pdf/2009.08553.pdf into collection: main File loaded successfully { "id": 18, "collection": "main", "extracted_text": null, "url": "//arxiv.org/pdf/2009.08553.pdf", "ingest_state": "pending", "ingest_error": null }
L'extraction du contenu d'un PDF volumineux peut prendre une minute ou deux, c'est pourquoi vous verrez souvent "ingest_state": "pending"
lorsque vous chargez pour la première fois un nouveau document.
dewy_qa query "tell me about RAG
Vous devriez voir quelque chose comme Querying main collection for: "tell me about RAG" Answer: Based on the given context, RAG refers to the RAG proteins, which are involved in DNA binding and V(D)J recombination. The RAG1 and RAG2 proteins work together to bind specific DNA sequences known as RSS (recombination signal sequences) and facilitate the cutting and rearrangement of DNA segments during the process of V(D)J recombination...