Dewy es una base de conocimientos de OSS diseñada para optimizar la forma en que los desarrolladores almacenan, organizan y recuperan información. Su flexibilidad y facilidad de uso lo convierten en una excelente opción para los desarrolladores que desean crear aplicaciones basadas en el conocimiento.
LangChain.js , por otro lado, es un marco poderoso que permite a los desarrolladores integrar LLM en sus aplicaciones sin problemas. Al combinar la gestión del conocimiento estructurado de Dewy con las capacidades LLM de LangChain.js, los desarrolladores pueden crear sofisticados sistemas de respuesta a preguntas que pueden comprender y procesar consultas complejas, ofreciendo respuestas precisas y contextualmente relevantes.
mkdir dewy_qa cd dewy_qa
Con el directorio configurado, puede instalar TypeScript e inicializar el proyecto: npm init -y npm i typescript --save-dev npx tsc --init
Dependiendo de su entorno, es posible que deba realizar algunos cambios en su configuración de TypeScript. Asegúrese de que su tsconfig.json
se parezca a lo siguiente:
{ "compilerOptions": { "target": "ES6", "module": "CommonJS", "moduleResolution": "node", "declaration": true, "outDir": "./dist", "esModuleInterop": true, "strict": true, }
Ahora está listo para crear la aplicación CLI. Para evitar que el código se ensucie demasiado, organícelo en varios directorios, con el siguiente diseño dewy_qa/ ├── commands/ │ └── ... ├── utils/ │ └── ... ├── index.ts ├── package.json └── tsconfig.ts
Cada comando se implementará en el directorio commands
y el código compartido irá al directorio utils
. El punto de entrada a la aplicación CLI es el archivo index.ts
.
Comience con una versión simple de "hola mundo" de index.ts
; comenzará a completarla en la siguiente sección
#!/usr/bin/env ts-node-script console.log("hello world");
Para verificar que el entorno esté configurado correctamente, intente ejecutar el siguiente comando; debería ver "hola mundo" impreso en la consola: npx ts-node index.ts
En lugar de escribir este comando muy largo cada vez, creemos una entrada en package.json
para el comando. Esto nos ayudará a recordar cómo invocar la CLI y facilitará la instalación como un comando:
{ ... "bin": { "dewy_qa": "./index.ts" } ... }
Ahora puede ejecutar su script con npm exec dewy_qa
o npm link
el paquete y ejecutarlo simplemente como dewy_qa
Cargue documentos configurando el cliente Dewy. El primer paso es agregar algunas dependencias al proyecto. El primero es dewy-ts
, la biblioteca cliente de Dewy. El segundo es commander
, que nos ayudará a crear una aplicación CLI con análisis de argumentos, subcomandos y más. Finalmente, chalk
para que las indicaciones sean más coloridas.
npm install dewy-ts commander chalk
A continuación, implemente la lógica del comando de carga. Hará esto en un archivo separado llamado commands/load.ts
. Este archivo implementa una función llamada load
, que espera una URL y algunas opciones adicionales; esto se conectará con la CLI en una sección posterior.
Dewy hace que la carga de documentos sea súper simple: simplemente configure el cliente y llame a addDocument
con la URL del archivo que desea cargar. Dewy se encarga de extraer el contenido del PDF, dividirlo en fragmentos del tamaño adecuado para enviarlos a un LLM e indexarlos para una búsqueda semántica.
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}`)); } }
Es posible que hayas notado que algunas funciones se importaron desde ../utils/colors
. Este archivo simplemente configura algunos asistentes para colorear la salida de la consola; colóquelo en utils
para que pueda usarse en otros lugares:
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);
Para comenzar, instale algunos paquetes adicionales: langchain
y openai
para usar la API de OpenAI como LLM:
npm install dewy-langchain langchain @langchain/openai openai
Lo primero que hay que configurar es Dewy (como antes) y un LLM. Una diferencia con respecto a antes es que dewy
se usa para construir un DewyRetriever
: este es un tipo especial usado por LangChain para recuperar información como parte de una cadena. Verás cómo se utiliza el retriever en tan sólo un minuto.
const model = new ChatOpenAI({ openAIApiKey: options.openai_api_key, }); const dewy = new Dewy({ BASE: options.dewy_endpoint }) const retriever = new DewyRetriever({ dewy, collection });
Esta es una plantilla de cadena que indica al LLM cómo debe comportarse, con marcadores de posición para contexto adicional que se proporcionarán cuando se cree la "cadena". En este caso, el LLM debe responder la pregunta, pero únicamente utilizando la información que se le proporciona. Esto reduce la tendencia del modelo a "alucinar" o inventar una respuesta que sea plausible pero incorrecta. Los valores de context
y question
se proporcionan en el siguiente paso:
const prompt = PromptTemplate.fromTemplate(`Answer the question based only on the following context: {context} Question: {question}`);
Utilice RunnableSequence
para crear una cadena LCEL. Esta cadena describe cómo generar el context
y los valores question
: el contexto se genera utilizando el recuperador creado anteriormente y la pregunta se genera pasando por la entrada del paso. Los resultados que recupera Dewy se formatean como una cadena canalizándolos a la función formatDocumentsAsString
.
DewyRetriever
, los asigna al context
y asigna el valor de entrada de la cadena a question
.context
y question
. const chain = RunnableSequence.from([ { context: retriever.pipe(formatDocumentsAsString), question: new RunnablePassthrough(), }, prompt, model, new StringOutputParser(), ]);
Ahora que se ha construido la cadena, ejecútela y envíe los resultados a la consola. Como verá, question
es un argumento de entrada proporcionado por quien llama a la función.
La ejecución de la cadena usando chain.streamLog()
le permite ver cada fragmento de respuesta tal como lo devuelve el LLM. El bucle del controlador de transmisión es algo feo, pero solo filtra los resultados de la transmisión apropiados y los escribe en STDOUT
(usando console.log
habría agregado nuevas líneas después de cada fragmento).
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); } } }
Ahora que ha visto todas las piezas, está listo para crear el comando query
. Esto debería ser similar al comando load
anterior, con algunas importaciones adicionales.
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}`)); } }
Con Dewy y LangChain.js integrados, el siguiente paso es crear la interfaz CLI. Utilice una biblioteca como commander
para crear una interfaz de línea de comandos fácil de usar que admita comandos para cargar documentos en Dewy y consultar la base de conocimientos utilizando LangChain.js.
Primero, reescriba index.ts
para crear los subcomandos load
y query
. El argumento --collection
determina en qué colección de Dewy se debe cargar el documento (Dewy le permite organizar documentos en diferentes colecciones, similar a las carpetas de archivos). El argumento --dewy-endpoint
le permite especificar cómo conectarse a Dewy; de forma predeterminada, se asume una instancia que se ejecuta localmente en el puerto 8000
. Finalmente, el argumento --openai_api_key
(que por defecto es una variable de entorno) configura la API de 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);
Bien, ya está todo hecho. ¿No fue tan fácil? Puedes probarlo ejecutando el comando: dewy_qa load //arxiv.org/pdf/2009.08553.pdf
Deberías ver algo como 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 }
Extraer el contenido de un PDF grande puede llevar uno o dos minutos, por lo que a menudo verá "ingest_state": "pending"
cuando cargue un documento nuevo por primera vez.
dewy_qa query "tell me about RAG
Deberías ver algo como 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...