![Search icon](https://hackernoon.imgix.net/search-new.png?w=19&h=19)
Nutidens store sprogmodeller har adgang til en stadigt voksende mængde information. Der er dog stadig en stor skare af private data, som disse modeller ikke udnytter. Dette er grunden til, at en af de mest populære applikationer af LLM'er i virksomhedsindstillinger er genfinding-augmented generation - RAG for kort. På
Du vil lære at bruge LangChain, den enormt populære ramme til at bygge RAG-systemer, til at bygge et simpelt RAG-system. Ved slutningen af øvelsen vil vi have en chatbot (med en Streamlit-grænseflade og det hele), der vil RAG sig vej gennem nogle private data for at give svar på spørgsmål.
For at præcisere, hvad RAG er, lad os overveje et simpelt eksempel.
En førsteårs universitetsstuderende, Chandler, overvejer at springe et par klasser over, men vil sikre sig, at han ikke overtræder politikken for universitetsdeltagelse. Som med alt i disse dage stiller han ChatGPT spørgsmålet.
ChatGPT kan selvfølgelig ikke besvare det. Chatbotten er ikke dum - den har bare ikke adgang til Chandlers universitetsdokumenter. Så Chandler finder selv politikdokumentet og opdager, at det er en lang, teknisk læsning, han ikke ønsker at vade igennem. I stedet giver han hele dokumentet til ChatGPT og stiller spørgsmålet igen. Denne gang får han sit svar.
Dette er et individuelt tilfælde af genfinding-augmented generation. Sprogmodellens svar (generation) er forstærket (beriget) af kontekst hentet fra en kilde, der ikke er en del af dens oprindelige træning.
En skalerbar version af et RAG-system ville være i stand til at besvare alle studerendes spørgsmål ved selv at søge i universitetsdokumenter, finde de relevante og hente tekststykker, der højst sandsynligt indeholder svaret.
Generelt kan man sige, at i et RAG-system henter man information fra en privat datakilde og leverer den til en sprogmodel, hvilket gør det muligt for modellen at give et kontekstuelt relevant svar.
Et sådant system ville, på trods af at det lyder ligetil, have mange bevægelige komponenter. Før vi bygger en selv, skal vi gennemgå, hvad de er, og hvordan de spiller sammen.
Den første komponent er et dokument eller en samling af dokumenter. Baseret på den type RAG-system, vi bygger, kan dokumenterne være tekstfiler, PDF'er, websider (RAG over ustrukturerede data) eller graf-, SQL- eller NoSQL-databaser (RAG over strukturerede data). De bruges til at indlæse forskellige typer data i systemet.
LangChain implementerer hundredvis af klasser kaldet dokumentindlæsere til at læse data fra forskellige dokumentkilder såsom PDF'er, Slack, Notion, Google Drive og så videre.
Hver dokumentindlæserklasse er unik, men de deler alle den samme .load()
-metode. For eksempel, her er, hvordan du kan indlæse et PDF-dokument og en webside i LangChain:
from langchain_community.document_loaders import PyPDFLoader, WebBaseLoader # pip install langchain-community pdf_loader = PyPDFLoader("framework_docs.pdf") web_loader = WebBaseLoader( "https://python.langchain.com/v0.2/docs/concepts/#document-loaders" ) pdf_docs = pdf_loader.load() web_docs = web_loader.load()
PyPDFLoader-klassen håndterer PDF-filer ved hjælp af PyPDF2-pakken under hætten, mens WebBaseLoader skraber det givne websideindhold.
pdf_docs
indeholder fire dokumentobjekter, et for hver side:
>>> len(pdf_docs) 4
Mens web_docs
kun indeholder én:
>>> print(web_docs[0].page_content[125:300].strip()) You can view the v0.1 docs here.IntegrationsAPI referenceLatestLegacyMorePeopleContributingCookbooks3rd party tutorialsYouTubearXivv0.2v0.2v0.1🦜️🔗LangSmithLangSmith DocsLangCh
Disse dokumentobjekter bliver senere givet til indlejring af modeller for at forstå den semantiske betydning bag deres tekst.
For detaljer om andre typer dokumentindlæsere tilbyder LangChain en
Når du har indlæst dine dokumenter, er det afgørende at opdele dem i mindre og mere overskuelige bidder af tekst. Her er hovedårsagerne:
LangChain tilbyder mange typer tekstsplittere under sin langchain_text_splitters-pakke, og de adskiller sig baseret på dokumenttype.
Her er, hvordan du bruger RecursiveCharacterTextSplitter
til at opdele almindelig tekst baseret på en liste over separatorer og chunkstørrelse:
!pip install langchain_text_splitters from langchain_text_splitters import RecursiveCharacterTextSplitter # Example text text = """ RAG systems combine the power of large language models with external knowledge sources. This allows them to provide up-to-date and context-specific information. The process involves several steps including document loading, text splitting, and embedding. """ # Create a text splitter text_splitter = RecursiveCharacterTextSplitter( chunk_size=50, chunk_overlap=10, length_function=len, separators=["\n\n", "\n", " ", ""], ) # Split the text chunks = text_splitter.split_text(text) # Print the chunks for i, chunk in enumerate(chunks): print(f"Chunk {i + 1}: {chunk}")
Produktion:
Chunk 1: RAG systems combine the power of large language Chunk 2: language models with external knowledge sources. Chunk 3: This allows them to provide up-to-date and Chunk 4: and context-specific information. Chunk 5: The process involves several steps including Chunk 6: including document loading, text splitting, and Chunk 7: and embedding.
Denne splitter er alsidig og fungerer godt til mange brugssager. Det opretter hver chunk med et tegnantal så tæt på chunk_size
som muligt. Den kan rekursivt skifte mellem hvilke separatorer, der skal opdeles ved for at holde tegntallet.
I ovenstående eksempel forsøger vores splitter først at opdele på nye linjer, derefter enkelte mellemrum og til sidst mellem alle tegn for at nå den ønskede chunk-størrelse.
Der er mange andre splittere i langchain_text_splitters
pakken. Her er nogle:
HTMLSectionSplitter
PythonCodeTexSplitter
RecursiveJsonSplitter
og så videre. Nogle af splitterne skaber semantisk meningsfulde bidder ved at bruge en transformermodel under hætten.
Den rigtige tekstsplitter har en væsentlig indflydelse på ydeevnen af et RAG-system.
For detaljer om, hvordan man bruger tekstdelere, se den relevante
Når dokumenter er opdelt i tekst, skal de kodes til deres numeriske repræsentation, hvilket er et krav for alle beregningsmodeller, der arbejder med tekstdata.
I forbindelse med RAG kaldes denne kodning indlejring og udføres ved indlejring af modeller . De skaber en vektorrepræsentation af et stykke tekst, der fanger deres semantiske betydning. Ved at præsentere tekst på denne måde kan du udføre matematiske operationer på dem, som at søge i vores dokumentdatabase efter tekst, der ligner mest i betydning eller finde et svar på en brugerforespørgsel.
LangChain understøtter alle større udbydere af indlejringsmodeller, såsom OpenAI, Cohere, HuggingFace og så videre. De er implementeret som Embedding
og giver to metoder: én til indlejring af dokumenter og én til indlejring af forespørgsler (prompter).
Her er et eksempel på en kode, der indlejrer de bidder af tekst, vi oprettede i det foregående afsnit ved hjælp af OpenAI:
from langchain_openai import OpenAIEmbeddings # Initialize the OpenAI embeddings embeddings = OpenAIEmbeddings() # Embed the chunks embedded_chunks = embeddings.embed_documents(chunks) # Print the first embedded chunk to see its structure print(f"Shape of the first embedded chunk: {len(embedded_chunks[0])}") print(f"First few values of the first embedded chunk: {embedded_chunks[0][:5]}")
Produktion:
Shape of the first embedded chunk: 1536 First few values of the first embedded chunk: [-0.020282309502363205, -0.0015041005099192262, 0.004193042870610952, 0.00229285703971982, 0.007068077567964792]
Outputtet ovenfor viser, at indlejringsmodellen skaber en 1536-dimensional vektor for alle bidder i vores dokumenter.
For at indlejre en enkelt forespørgsel kan du bruge metoden embed_query()
:
query = "What is RAG?" query_embedding = embeddings.embed_query(query) print(f"Shape of the query embedding: {len(query_embedding)}") print(f"First few values of the query embedding: {query_embedding[:5]}")
Produktion:
Shape of the query embedding: 1536 First few values of the query embedding: [-0.012426204979419708, -0.016619959846138954, 0.007880032062530518, -0.0170428603887558, 0.011404196731746197]
I storstilede RAG-applikationer, hvor du måske har gigabyte af dokumenter, vil du ende med gazillioner tekstbidder og dermed vektorer. Der er ingen brug for dem, hvis du ikke kan opbevare dem pålideligt.
Dette er grunden til, at vektorlagre eller databaser er i høj kurs nu. Udover at gemme dine indlejringer, sørger vektordatabaser for at udføre vektorsøgning for dig. Disse databaser er optimeret til hurtigt at finde de mest lignende vektorer, når de får en forespørgselsvektor, hvilket er afgørende for at hente relevant information i RAG-systemer.
Her er et kodestykke, der indlejrer indholdet af en webside og gemmer vektorerne i en Chroma vektordatabase ( Chroma er en open source vektordatabaseløsning , der kører udelukkende på din maskine):
!pip install chromadb langchain_chroma from langchain_community.document_loaders import WebBaseLoader from langchain_text_splitters import RecursiveCharacterTextSplitter # Load the web page loader = WebBaseLoader("https://python.langchain.com/v0.2/docs/tutorials/rag/") docs = loader.load() # Split the documents into chunks text_splitter = RecursiveCharacterTextSplitter(chunk_size=1000, chunk_overlap=200) chunks = text_splitter.split_documents(docs)
Først indlæser vi siden med WebBaseLoader
og opretter vores chunks. Derefter kan vi sende bidderne direkte til from_documents
metoden i Chroma
sammen med vores foretrukne indlejringsmodel:
from langchain_openai import OpenAIEmbeddings from langchain_chroma import Chroma db = Chroma.from_documents(chunks, OpenAIEmbeddings())
Alle vektordatabaseobjekter i LangChain afslører en similarity_search
metode, der accepterer en forespørgselsstreng:
query = "What is indexing in the context of RAG?" docs = db.similarity_search(query) print(docs[1].page_content)
Produktion:
If you are interested for RAG over structured data, check out our tutorial on doing question/answering over SQL data.ConceptsA typical RAG application has two main components:Indexing: a pipeline for ingesting data from a source and indexing it. This usually happens offline.Retrieval and generation: the actual RAG chain, which takes the user query at run time and retrieves the relevant data from the index, then passes that to the model.The most common full sequence from raw data to answer looks like:IndexingLoad: First we need to load our data. This is done with Document Loaders.Split: Text splitters break large Documents into smaller chunks. This is useful both for indexing data and for passing it in to a model, since large chunks are harder to search over and won't fit in a model's finite context window.Store: We need somewhere to store and index our splits, so that they can later be searched over. This is often done using a VectorStore and Embeddings model.Retrieval and
Resultatet af similarity_search
er en liste over dokumenter, der højst sandsynligt indeholder de oplysninger, vi beder om i forespørgslen.
For detaljer om, hvordan man bruger vektorlagre, se de relevante
Selvom alle vektorlagre understøtter hentning i form af lighedssøgning, implementerer LangChain en dedikeret Retriever
grænseflade, der returnerer dokumenter givet en ustruktureret forespørgsel. En retriever skal kun returnere eller hente dokumenter, ikke gemme dem.
Sådan kan du konvertere ethvert vektorlager til en retriever i LangChain:
# Convert the vector store to a retriever chroma_retriever = db.as_retriever() docs = chroma_retriever.invoke("What is indexing in the context of RAG?") >>> len(docs) 4
Det er muligt at begrænse antallet af relevante dokumenter til top k ved hjælp af search_kwargs
:
chroma_retriever = db.as_retriever(search_kwargs={"k": 1}) docs = chroma_retriever.invoke("What is indexing in the context of RAG?") >>> len(docs) 1
Du kan videregive andre søgerelaterede parametre til search_kwargs. Lær mere om brug af retrievere fra
Nu hvor vi har dækket nøglekomponenterne i et RAG-system, vil vi bygge et selv. Jeg vil lede dig gennem en trin-for-trin implementering af en RAG-chatbot designet specifikt til kodedokumentation og tutorials. Du vil finde det særligt nyttigt, når du har brug for AI-kodningsassistance til nye rammer eller nye funktioner i eksisterende rammer, som endnu ikke er en del af videnbasen i nutidens LLM'er.
Først skal du udfylde din arbejdsmappe med følgende projektstruktur:
rag-chatbot/ ├── .gitignore ├── requirements.txt ├── README.md ├── app.py ├── src/ │ ├── __init__.py │ ├── document_processor.py │ └── rag_chain.py └── .streamlit/ └── config.toml
Her er kommandoerne:
$ touch .gitignore requirements.txt README.md app.py $ mkdir src .streamlit $ touch src/{.env,__init__.py,document_processor.py,rag_chain.py} $ touch .streamlit/{.env,config.toml}
I dette trin opretter du først et nyt Conda-miljø og aktiverer det:
$ conda create -n rag_tutorial python=3.9 -y $ conda activate rag_tutorial
Åbn derefter filen requirements.txt
og indsæt følgende afhængigheder:
langchain==0.2.14 langchain_community==0.2.12 langchain_core==0.2.35 langchain_openai==0.1.22 python-dotenv==1.0.1 streamlit==1.37.1 faiss-cpu pypdf
og installer dem:
$ pip install -r requirements.txt
Opret også en .gitignore
-fil for at skjule filer fra git-indeksering:
# .gitignore venv/ __pycache__/ .env *.pdf *.png *.jpg *.jpeg *.gif *.svg
Åbn derefter filen src/document_processor.py
og indsæt de kommende kodestykker.
Den nødvendige import:
import logging from langchain.text_splitter import RecursiveCharacterTextSplitter from langchain.text_splitter import Language from langchain_community.document_loaders import PyPDFLoader from langchain_community.document_loaders.parsers.pdf import ( extract_from_images_with_rapidocr, ) from langchain.schema import Document
Forklaring af importen:
RecursiveCharacterTextSplitter
: Opdeler tekst i mindre bidder rekursivt.Language
: Enum for specificering af programmeringssprog i tekstopdeling.PyPDFLoader
: Indlæser og udtrækker tekst fra PDF-filer.extract_from_images_with_rapidocr
: OCR-funktion til at udtrække tekst fra billeder.Document
: Repræsenterer et dokument med indhold og metadata.logging
: Giver logfunktionalitet til fejlretning og information.
Derefter en funktion til behandling af PDF'er:
def process_pdf(source): loader = PyPDFLoader(source) documents = loader.load() # Filter out scanned pages unscanned_documents = [doc for doc in documents if doc.page_content.strip() != ""] scanned_pages = len(documents) - len(unscanned_documents) if scanned_pages > 0: logging.info(f"Omitted {scanned_pages} scanned page(s) from the PDF.") if not unscanned_documents: raise ValueError( "All pages in the PDF appear to be scanned. Please use a PDF with text content." ) return split_documents(unscanned_documents)
Sådan fungerer det:
PyPDFLoader
. Funktionen håndterer tilfælde, hvor en PDF-fil kan indeholde en blanding af tekst og scannede sider, hvilket sikrer, at kun tekstbaserede sider behandles videre. Dette er afgørende for tekstanalyseopgaver, hvor scannede sider uden OCR ville være ubrugelige. Vi vil definere split_documents
funktionen senere.
Dernæst skriver vi en funktion til at hente information fra billeder (skærmbilleder af kodestykker og/eller websider):
def process_image(source): # Extract text from image using OCR with open(source, "rb") as image_file: image_bytes = image_file.read() extracted_text = extract_from_images_with_rapidocr([image_bytes]) documents = [Document(page_content=extracted_text, metadata={"source": source})] return split_documents(documents)
Denne funktion behandler en billedfil ved at udtrække tekst ved hjælp af OCR (Optical Character Recognition). Den læser billedfilen, konverterer den til bytes og bruger derefter RapidOCR-biblioteket til at udtrække tekst fra billedet. Den udpakkede tekst pakkes derefter ind i et dokumentobjekt med metadata, der indeholder kildefilstien. Endelig opdeler funktionen dokumentet i mindre bidder ved hjælp af split_documents
funktionen, som vi definerer herefter:
def split_documents(documents): # Split documents into smaller chunks for processing text_splitter = RecursiveCharacterTextSplitter.from_language( language=Language.PYTHON, chunk_size=1000, chunk_overlap=200 ) return text_splitter.split_documents(documents)
Funktionen bruger klassen RecursiveCharacterTextSplitter med Pythons syntaks til at opdele tekst i bidder af 1000 tegn og 200 tegn overlap.
Vores sidste funktion kombinerer PDF- og billedparserfunktionerne til én:
def process_document(source): # Determine file type and process accordingly if source.lower().endswith(".pdf"): return process_pdf(source) elif source.lower().endswith((".png", ".jpg", ".jpeg")): return process_image(source) else: raise ValueError(f"Unsupported file type: {source}")
Denne sidste funktion vil blive brugt af Streamlit UI ned ad linjen til at oprette, indlejre og gemme bidder fra leverede dokumenter og videregive dem til RAG-komponenten i vores system.
Åbn nu filen src/rag_chain.py
og indsæt de kommende kodestykker.
Importer først nødvendige moduler:
import os from dotenv import load_dotenv from langchain.prompts import PromptTemplate from langchain_community.vectorstores import FAISS from langchain_core.output_parsers import StrOutputParser from langchain_core.runnables import RunnablePassthrough from langchain_openai import ChatOpenAI, OpenAIEmbeddings # Load the API key from env variables load_dotenv() api_key = os.getenv("OPENAI_API_KEY")
Her er en forklaring på importen:
• os
: Operativsysteminteraktioner • dotenv
: Indlæs miljøvariabler • langchain
:
PromptTemplate
: Oprettelse af brugerdefineret promptFAISS
: Et letvægts vektorlager til dokumenterStrOutputParser
: Konvertering af LLM-meddelelsesobjekter til strengoutputRunnablePassthrough
: Opret komponerbare kæderChatOpenAI
, OpenAIEmbeddings
: OpenAI-modelinteraktioner
Dernæst opretter vi vores prompt til RAG-systemet:
RAG_PROMPT_TEMPLATE = """ You are a helpful coding assistant that can answer questions about the provided context. The context is usually a PDF document or an image (screenshot) of a code file. Augment your answers with code snippets from the context if necessary. If you don't know the answer, say you don't know. Context: {context} Question: {question} """ PROMPT = PromptTemplate.from_template(RAG_PROMPT_TEMPLATE)
RAG-systemprompt er en af de kritiske faktorer for dets succes. Vores version er enkel, men vil få arbejdet gjort det meste af tiden. I praksis ville du bruge meget tid på at gentage og forbedre på prompten.
Hvis du bemærker, bruger vi en PromptTemplate
klasse til at konstruere prompten. Denne konstruktion giver os mulighed for dynamisk at indtage konteksten hentet fra dokumenter og brugerens forespørgsel til en endelig prompt.
Når vi taler om dokumenter, har vi brug for en funktion til at formatere dem, før de overføres som kontekst til systemprompten:
def format_docs(docs): return "\n\n".join(doc.page_content for doc in docs)
Det er en simpel funktion, der sammenkæder sideindholdet i hentede dokumenter.
Til sidst opretter vi en funktion, der skal udvikle vores RAG-kæde:
def create_rag_chain(chunks): embeddings = OpenAIEmbeddings(api_key=api_key) doc_search = FAISS.from_documents(chunks, embeddings) retriever = doc_search.as_retriever( search_type="similarity", search_kwargs={"k": 5} ) llm = ChatOpenAI(model_name="gpt-4o-mini", temperature=0) rag_chain = ( {"context": retriever | format_docs, "question": RunnablePassthrough()} | PROMPT | llm | StrOutputParser() ) return rag_chain
Funktionen accepterer dokumentbidder, som leveres af process_document
funktionen inde i scriptet document_processor.py
.
Funktionen starter med at definere indlejringsmodellen og gemme dokumenterne i et FAISS vektorlager. Derefter konverteres den til retriever-grænsefladen med lighedssøgning, der returnerer top fem dokumenter, der matcher brugerens forespørgsel.
Til sprogmodellen vil vi bruge gpt-4o-mini
, men du kan bruge andre modeller som GPT-4o afhængigt af dit budget og behov.
Derefter vil vi sætte alle disse komponenter sammen ved hjælp af LangChain Expression Language (LCEL). Den første komponent i kæden er en ordbog med context
og question
som nøgler. Værdierne af disse nøgler leveres af retrieveren, der er formateret af henholdsvis vores formateringsfunktion og RunnablePassthrough()
. Sidstnævnte klasse fungerer som en pladsholder for brugerens forespørgsel.
Ordbogen sendes derefter til vores systemprompt; prompten føres til LLM, som genererer output-meddelelsesklasse. Meddelelsesklassen gives til en strengoutputparser, der returnerer et almindeligt tekstsvar.
I dette afsnit vil vi bygge nedenstående brugergrænseflade til vores app:
Det er en ren, minimal grænseflade med to inputfelter - et til dokumentet, det andet til at stille spørgsmål om dokumentet. I venstre sidebjælke bliver brugeren bedt om at indtaste deres API-nøgle.
For at bygge grænsefladen skal du åbne app.py
scriptet i det øverste niveau af din arbejdsmappe og indsætte følgende kode:
import streamlit as st import os from dotenv import load_dotenv from src.document_processor import process_document from src.rag_chain import create_rag_chain # Load environment variables load_dotenv() st.set_page_config(page_title="RAG Chatbot", page_icon="🤖") st.title("RAG Chatbot") # Initialize session state if "rag_chain" not in st.session_state: st.session_state.rag_chain = None # Sidebar for API key input with st.sidebar: api_key = st.text_input("Enter your OpenAI API Key", type="password") if api_key: os.environ["OPENAI_API_KEY"] = api_key # File uploader uploaded_file = st.file_uploader("Choose a file", type=["pdf", "png", "jpg", "jpeg"]) if uploaded_file is not None: if st.button("Process File"): if api_key: with st.spinner("Processing file..."): # Save the uploaded file temporarily with open(uploaded_file.name, "wb") as f: f.write(uploaded_file.getbuffer()) try: # Process the document chunks = process_document(uploaded_file.name) # Create RAG chain st.session_state.rag_chain = create_rag_chain(chunks) st.success("File processed successfully!") except ValueError as e: st.error(str(e)) finally: # Remove the temporary file os.remove(uploaded_file.name) else: st.error("Please provide your OpenAI API key.") # Query input query = st.text_input("Ask a question about the uploaded document") if st.button("Ask"): if st.session_state.rag_chain and query: with st.spinner("Generating answer..."): result = st.session_state.rag_chain.invoke(query) st.subheader("Answer:") st.write(result) elif not st.session_state.rag_chain: st.error("Please upload and process a file first.") else: st.error("Please enter a question.")
På trods af at den kun er 65 linjer lang, implementerer den følgende funktionalitet:
Der er kun ét trin tilbage – at implementere vores Streamlit-app. Der er mange muligheder her, men den nemmeste måde er at bruge Streamlit Cloud, som er gratis og nem at sætte op.
Åbn først .streamlit/config.toml
-scriptet og indsæt følgende konfigurationer:
[theme] primaryColor = "#F63366" backgroundColor = "#FFFFFF" secondaryBackgroundColor = "#F0F2F6" textColor = "#262730" font = "sans serif"
Dette er nogle temajusteringer, der kommer fra personlige præferencer. Skriv derefter filen README.md (du kan kopiere dens indhold fra denne hostede fil på GitHub ).
Gå endelig til GitHub.com og opret et nyt lager. Kopier dets link og vend tilbage til din arbejdsmappe:
$ git init $ git add . $ git commit -m "Initial commit" $ git remote add origin https://github.com/YourUsername/YourRepo.git $ git push --set-upstream origin master
Ovenstående kommandoer initialiserer Git, opret en indledende commit og skub alt til depotet (glem ikke at erstatte repo-linket med dit eget).
Nu skal du tilmelde dig en gratis konto hos Streamlit Cloud . Tilslut din GitHub-konto, og vælg det lager, der indeholder din app.
Konfigurer derefter appindstillingerne:
app.py
OPENAI_API_KEY
) i appindstillingerne
Klik til sidst på "Deploy"!
Appen skal være operationel inden for få minutter. Den app, jeg har bygget til denne tutorial, kan findes på dette link . Prøv det!
Denne tutorial ser på den potente blanding af Retrieval-Augmented Generation (RAG) og Streamlit, der danner et interaktivt spørgsmål-svar-system baseret på dokumenter. Det tager læseren gennem hele processen, fra opsætning af et miljø og behandling af dokumenter til opbygning af en RAG-kæde og implementering af en venlig web-app.
Vigtige punkter omfatter:
Dette projekt danner grundlag for ansøgninger, der er mere avancerede. Det kan udvides på væsentlige måder, såsom inkorporering af flere dokumenttyper, forbedret genfindingsnøjagtighed og funktioner som dokumentresumé. Og alligevel, hvad det virkelig tjener, er som en demonstration af den potentielle kraft af disse teknologier, individuelt og kombineret.