From 153c51e1c75f98fb0cc63124ace7f3bc98af6e3a Mon Sep 17 00:00:00 2001 From: andrewfulton9 Date: Fri, 10 Jan 2025 18:14:18 +0000 Subject: [PATCH 1/8] add components --- ragna/core/_components.py | 45 +++++++++++++++++++++++++++++++++++++++ ragna/core/_rag.py | 17 +++++++++++++-- 2 files changed, 60 insertions(+), 2 deletions(-) diff --git a/ragna/core/_components.py b/ragna/core/_components.py index fc0d5e44..1ff4f80a 100644 --- a/ragna/core/_components.py +++ b/ragna/core/_components.py @@ -5,12 +5,14 @@ import functools import inspect import uuid +from dataclasses import dataclass, field from datetime import datetime, timezone from typing import ( Any, AsyncIterable, AsyncIterator, Iterator, + List, Optional, Type, Union, @@ -302,3 +304,46 @@ def answer(self, messages: list[Message]) -> Iterator[str]: Answer. """ ... + + +@dataclass +class QueryProcessingStep: + original_query: str + processed_query: str + metadata_filter: Optional[MetadataFilter] = None + processor_name: str = "" + metadata: dict = field(default_factory=dict) + + +@dataclass +class ProcessedQuery: + retrieval_query: str + answer_query: str + metadata_filter: MetadataFilter + processing_history: List[QueryProcessingStep] = field(default_factory=list) + + +class QueryPreprocessor(abc.ABC): + @abc.abstractmethod + def process( + self, query: str, metadata_filter: Optional[MetadataFilter] = None + ) -> ProcessedQuery: + pass + + @classmethod + def display_name(cls) -> str: + return cls.__name__ + + +class DefaultQueryPreprocessor(QueryPreprocessor): + def process( + self, query: str, metadata_filter: Optional[MetadataFilter] = None + ) -> ProcessedQuery: + return ProcessedQuery( + retrieval_query=query, + answer_query=query, + metadata_filter=metadata_filter or MetadataFilter(), + processing_history=[ + QueryProcessingStep(query, query, metadata_filter, self.display_name()) + ], + ) diff --git a/ragna/core/_rag.py b/ragna/core/_rag.py index 121f39a0..7bae7746 100644 --- a/ragna/core/_rag.py +++ b/ragna/core/_rag.py @@ -28,7 +28,15 @@ from ragna._utils import as_async_iterator, as_awaitable, default_user -from ._components import Assistant, Component, Message, MessageRole, SourceStorage +from ._components import ( + Assistant, + Component, + DefaultQueryPreprocessor, + Message, + MessageRole, + QueryPreprocessor, + SourceStorage, +) from ._document import Document, LocalDocument from ._metadata_filter import MetadataFilter from ._utils import RagnaException, merge_models @@ -256,6 +264,7 @@ def __init__( self._unpacked_params = self._unpack_chat_params(params) self._messages: list[Message] = [] + self.preprocessor: QueryPreprocessor = DefaultQueryPreprocessor() async def prepare(self) -> Message: """Prepare the chat. @@ -300,8 +309,12 @@ async def answer(self, prompt: str, *, stream: bool = False) -> Message: http_detail=RagnaException.EVENT, ) + processed = self.preprocessor.process(prompt, self.metadata_filter) sources = await self._as_awaitable( - self.source_storage.retrieve, self.corpus_name, self.metadata_filter, prompt + self.source_storage.retrieve, + self.corpus_name, + processed.metadata_filter, + processed.retrieval_query, ) if not sources: event = "Unable to retrieve any sources." From ed1e3e84f96542cb1773f170477234cfa942596d Mon Sep 17 00:00:00 2001 From: andrewfulton9 Date: Wed, 15 Jan 2025 23:32:56 +0000 Subject: [PATCH 2/8] dev --- ragna/core/_components.py | 41 +++++++++++++-------------------------- ragna/core/_rag.py | 10 +++++++--- 2 files changed, 21 insertions(+), 30 deletions(-) diff --git a/ragna/core/_components.py b/ragna/core/_components.py index 1ff4f80a..1f1eb750 100644 --- a/ragna/core/_components.py +++ b/ragna/core/_components.py @@ -5,7 +5,7 @@ import functools import inspect import uuid -from dataclasses import dataclass, field +from dataclasses import field from datetime import datetime, timezone from typing import ( Any, @@ -306,8 +306,7 @@ def answer(self, messages: list[Message]) -> Iterator[str]: ... -@dataclass -class QueryProcessingStep: +class QueryProcessingStep(pydantic.BaseModel): original_query: str processed_query: str metadata_filter: Optional[MetadataFilter] = None @@ -315,35 +314,23 @@ class QueryProcessingStep: metadata: dict = field(default_factory=dict) -@dataclass -class ProcessedQuery: - retrieval_query: str - answer_query: str +class ProcessedQuery(pydantic.BaseModel): + """original query is the query as it was passed to the preprocessor. + processed query is the query after each step of the processing pipeline. + metadata_filter is the metadata filter that was applied to the query.""" + + original_query: str + processed_query: str metadata_filter: MetadataFilter processing_history: List[QueryProcessingStep] = field(default_factory=list) -class QueryPreprocessor(abc.ABC): - @abc.abstractmethod - def process( - self, query: str, metadata_filter: Optional[MetadataFilter] = None - ) -> ProcessedQuery: - pass +class QueryPreprocessor(Component, abc.ABC): + """Abstract base class for query preprocessors.""" - @classmethod - def display_name(cls) -> str: - return cls.__name__ - - -class DefaultQueryPreprocessor(QueryPreprocessor): + @abc.abstractmethod def process( self, query: str, metadata_filter: Optional[MetadataFilter] = None ) -> ProcessedQuery: - return ProcessedQuery( - retrieval_query=query, - answer_query=query, - metadata_filter=metadata_filter or MetadataFilter(), - processing_history=[ - QueryProcessingStep(query, query, metadata_filter, self.display_name()) - ], - ) + """Preprocess a query.""" + ... diff --git a/ragna/core/_rag.py b/ragna/core/_rag.py index 7bae7746..f45cb7cf 100644 --- a/ragna/core/_rag.py +++ b/ragna/core/_rag.py @@ -31,7 +31,6 @@ from ._components import ( Assistant, Component, - DefaultQueryPreprocessor, Message, MessageRole, QueryPreprocessor, @@ -248,6 +247,7 @@ def __init__( source_storage: SourceStorage, assistant: Assistant, corpus_name: str = "default", + preprocessor: QueryPreprocessor = Optional[QueryPreprocessor], **params: Any, ) -> None: self._rag = rag @@ -256,6 +256,7 @@ def __init__( self.source_storage = source_storage self.assistant = assistant self.corpus_name = corpus_name + self.preprocessor = preprocessor special_params = SpecialChatParams().model_dump() special_params.update(params) @@ -264,7 +265,7 @@ def __init__( self._unpacked_params = self._unpack_chat_params(params) self._messages: list[Message] = [] - self.preprocessor: QueryPreprocessor = DefaultQueryPreprocessor() + self.preprocessor: QueryPreprocessor = preprocessor async def prepare(self) -> Message: """Prepare the chat. @@ -308,8 +309,11 @@ async def answer(self, prompt: str, *, stream: bool = False) -> Message: http_status_code=status.HTTP_400_BAD_REQUEST, http_detail=RagnaException.EVENT, ) + if self.preprocessor is not None: + processed = self.preprocessor.process(prompt, self.metadata_filter) + prompt = processed.answer_query + metadata_filter = processed.metadata_filter - processed = self.preprocessor.process(prompt, self.metadata_filter) sources = await self._as_awaitable( self.source_storage.retrieve, self.corpus_name, From 1492a7e60c6f89ee473ef2b5e4273333da3692d1 Mon Sep 17 00:00:00 2001 From: andrewfulton9 Date: Thu, 16 Jan 2025 17:41:53 +0000 Subject: [PATCH 3/8] basic example working --- ragna/core/__init__.py | 6 ++++++ ragna/core/_components.py | 1 - ragna/core/_rag.py | 14 ++++++++------ ragna/preprocessors/__init__.py | 10 ++++++++++ ragna/preprocessors/_demo.py | 32 ++++++++++++++++++++++++++++++++ 5 files changed, 56 insertions(+), 7 deletions(-) create mode 100644 ragna/preprocessors/__init__.py create mode 100644 ragna/preprocessors/_demo.py diff --git a/ragna/core/__init__.py b/ragna/core/__init__.py index 7e297275..e2d3a117 100644 --- a/ragna/core/__init__.py +++ b/ragna/core/__init__.py @@ -21,6 +21,9 @@ "Source", "SourceStorage", "PlainTextDocumentHandler", + "QueryProcessingStep", + "ProcessedQuery", + "QueryPreprocessor", ] from ._utils import ( @@ -51,6 +54,9 @@ Component, Message, MessageRole, + ProcessedQuery, + QueryPreprocessor, + QueryProcessingStep, Source, SourceStorage, ) diff --git a/ragna/core/_components.py b/ragna/core/_components.py index 1f1eb750..d43043f0 100644 --- a/ragna/core/_components.py +++ b/ragna/core/_components.py @@ -311,7 +311,6 @@ class QueryProcessingStep(pydantic.BaseModel): processed_query: str metadata_filter: Optional[MetadataFilter] = None processor_name: str = "" - metadata: dict = field(default_factory=dict) class ProcessedQuery(pydantic.BaseModel): diff --git a/ragna/core/_rag.py b/ragna/core/_rag.py index f45cb7cf..88b629d1 100644 --- a/ragna/core/_rag.py +++ b/ragna/core/_rag.py @@ -155,6 +155,7 @@ def chat( *, source_storage: Union[SourceStorage, type[SourceStorage], str], assistant: Union[Assistant, type[Assistant], str], + preprocessor: Optional[QueryPreprocessor] = None, corpus_name: str = "default", **params: Any, ) -> Chat: @@ -179,6 +180,7 @@ def chat( input=input, source_storage=cast(SourceStorage, self._load_component(source_storage)), # type: ignore[arg-type] assistant=cast(Assistant, self._load_component(assistant)), # type: ignore[arg-type] + preprocessor=preprocessor, # cast(preprocessor, self._load_component(preprocessor)), # type: ignore[arg-type] corpus_name=corpus_name, **params, ) @@ -246,8 +248,8 @@ def __init__( *, source_storage: SourceStorage, assistant: Assistant, + preprocessor: QueryPreprocessor = None, corpus_name: str = "default", - preprocessor: QueryPreprocessor = Optional[QueryPreprocessor], **params: Any, ) -> None: self._rag = rag @@ -310,15 +312,15 @@ async def answer(self, prompt: str, *, stream: bool = False) -> Message: http_detail=RagnaException.EVENT, ) if self.preprocessor is not None: - processed = self.preprocessor.process(prompt, self.metadata_filter) - prompt = processed.answer_query - metadata_filter = processed.metadata_filter + processed = self.preprocessor().process(prompt, self.metadata_filter) + prompt = processed.processed_query + self.metadata_filter = processed.metadata_filter sources = await self._as_awaitable( self.source_storage.retrieve, self.corpus_name, - processed.metadata_filter, - processed.retrieval_query, + self.metadata_filter, + prompt, ) if not sources: event = "Unable to retrieve any sources." diff --git a/ragna/preprocessors/__init__.py b/ragna/preprocessors/__init__.py new file mode 100644 index 00000000..84947b4a --- /dev/null +++ b/ragna/preprocessors/__init__.py @@ -0,0 +1,10 @@ +__all__ = [ + "RagnaDemoPreprocessor", +] + +from ragna._utils import fix_module + +from ._demo import RagnaDemoPreprocessor + +fix_module(globals()) +del fix_module diff --git a/ragna/preprocessors/_demo.py b/ragna/preprocessors/_demo.py new file mode 100644 index 00000000..5c4a1fcb --- /dev/null +++ b/ragna/preprocessors/_demo.py @@ -0,0 +1,32 @@ +from typing import Optional + +from ragna.core import ( + MetadataFilter, + ProcessedQuery, + QueryPreprocessor, + QueryProcessingStep, +) + + +class RagnaDemoPreprocessor(QueryPreprocessor): + def process( + self, query: str, metadata_filter: Optional[MetadataFilter] = None + ) -> ProcessedQuery: + """Retrieval query is the original query, answer query is the processed query.""" + print(query) + print(metadata_filter) + processed_query = """This is a demo preprocessor. It doesn't do anything to the query. original query: """ + processed_query += query + return ProcessedQuery( + original_query=query, + processed_query=processed_query, + metadata_filter=metadata_filter or MetadataFilter(), + processing_history=[ + QueryProcessingStep( + original_query=query, + processed_query=query, + metadata_filter=metadata_filter, + processor_name=self.display_name(), + ) + ], + ) From e35def6e977ba7ed9fd8f15fb90975dc4e166026 Mon Sep 17 00:00:00 2001 From: andrewfulton9 Date: Fri, 17 Jan 2025 00:42:45 +0000 Subject: [PATCH 4/8] Basic preprocessing demo --- preprocessing_demo.ipynb | 294 ++++++++++++++++++++++++++++++++ ragna/core/_components.py | 2 +- ragna/core/_rag.py | 5 +- ragna/preprocessors/_demo.py | 4 +- tests/preprocessors/__init__.py | 0 5 files changed, 298 insertions(+), 7 deletions(-) create mode 100644 preprocessing_demo.ipynb create mode 100644 tests/preprocessors/__init__.py diff --git a/preprocessing_demo.ipynb b/preprocessing_demo.ipynb new file mode 100644 index 00000000..cb8e1bb1 --- /dev/null +++ b/preprocessing_demo.ipynb @@ -0,0 +1,294 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "id": "192e7b22-47c8-41f5-ae1b-f63b0ac5131d", + "metadata": { + "scrolled": true + }, + "outputs": [], + "source": [ + "%load_ext autoreload\n", + "%autoreload 2" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "27a0556d-5fa3-48aa-94df-ee5b916ebcdd", + "metadata": { + "scrolled": true + }, + "outputs": [ + { + "data": { + "text/plain": [ + "[PosixPath('/home/andrew/.dropbox-hm/Dropbox/quansight/dev/ragna/ragna/docs/install.md'),\n", + " PosixPath('/home/andrew/.dropbox-hm/Dropbox/quansight/dev/ragna/ragna/docs/index.md'),\n", + " PosixPath('/home/andrew/.dropbox-hm/Dropbox/quansight/dev/ragna/ragna/docs/community/welcome.md'),\n", + " PosixPath('/home/andrew/.dropbox-hm/Dropbox/quansight/dev/ragna/ragna/docs/community/contribute.md'),\n", + " PosixPath('/home/andrew/.dropbox-hm/Dropbox/quansight/dev/ragna/ragna/docs/explanations/what-is-rag.md'),\n", + " PosixPath('/home/andrew/.dropbox-hm/Dropbox/quansight/dev/ragna/ragna/docs/examples/README.md'),\n", + " PosixPath('/home/andrew/.dropbox-hm/Dropbox/quansight/dev/ragna/ragna/docs/references/python-api.md'),\n", + " PosixPath('/home/andrew/.dropbox-hm/Dropbox/quansight/dev/ragna/ragna/docs/references/deploy.md'),\n", + " PosixPath('/home/andrew/.dropbox-hm/Dropbox/quansight/dev/ragna/ragna/docs/references/config.md'),\n", + " PosixPath('/home/andrew/.dropbox-hm/Dropbox/quansight/dev/ragna/ragna/docs/references/release-notes.md'),\n", + " PosixPath('/home/andrew/.dropbox-hm/Dropbox/quansight/dev/ragna/ragna/docs/references/faq.md'),\n", + " PosixPath('/home/andrew/.dropbox-hm/Dropbox/quansight/dev/ragna/ragna/docs/tutorials/README.md')]" + ] + }, + "execution_count": 2, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from pathlib import Path\n", + "\n", + "docs_path = Path.cwd().joinpath(\"docs\")\n", + "\n", + "md_files = list(docs_path.glob(\"**/*.md\"))" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "8c2c65be-03fb-45dd-b9a2-4c9684e09b83", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Collecting openai\n", + " Downloading openai-1.59.7-py3-none-any.whl.metadata (27 kB)\n", + "Requirement already satisfied: anyio<5,>=3.5.0 in /home/andrew/micromamba/envs/ragna-dev/lib/python3.10/site-packages (from openai) (4.8.0)\n", + "Collecting distro<2,>=1.7.0 (from openai)\n", + " Using cached distro-1.9.0-py3-none-any.whl.metadata (6.8 kB)\n", + "Requirement already satisfied: httpx<1,>=0.23.0 in /home/andrew/micromamba/envs/ragna-dev/lib/python3.10/site-packages (from openai) (0.28.1)\n", + "Collecting jiter<1,>=0.4.0 (from openai)\n", + " Downloading jiter-0.8.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.metadata (5.2 kB)\n", + "Requirement already satisfied: pydantic<3,>=1.9.0 in /home/andrew/micromamba/envs/ragna-dev/lib/python3.10/site-packages (from openai) (2.10.5)\n", + "Requirement already satisfied: sniffio in /home/andrew/micromamba/envs/ragna-dev/lib/python3.10/site-packages (from openai) (1.3.1)\n", + "Requirement already satisfied: tqdm>4 in /home/andrew/micromamba/envs/ragna-dev/lib/python3.10/site-packages (from openai) (4.67.1)\n", + "Requirement already satisfied: typing-extensions<5,>=4.11 in /home/andrew/micromamba/envs/ragna-dev/lib/python3.10/site-packages (from openai) (4.12.2)\n", + "Requirement already satisfied: exceptiongroup>=1.0.2 in /home/andrew/micromamba/envs/ragna-dev/lib/python3.10/site-packages (from anyio<5,>=3.5.0->openai) (1.2.2)\n", + "Requirement already satisfied: idna>=2.8 in /home/andrew/micromamba/envs/ragna-dev/lib/python3.10/site-packages (from anyio<5,>=3.5.0->openai) (3.10)\n", + "Requirement already satisfied: certifi in /home/andrew/micromamba/envs/ragna-dev/lib/python3.10/site-packages (from httpx<1,>=0.23.0->openai) (2024.12.14)\n", + "Requirement already satisfied: httpcore==1.* in /home/andrew/micromamba/envs/ragna-dev/lib/python3.10/site-packages (from httpx<1,>=0.23.0->openai) (1.0.7)\n", + "Requirement already satisfied: h11<0.15,>=0.13 in /home/andrew/micromamba/envs/ragna-dev/lib/python3.10/site-packages (from httpcore==1.*->httpx<1,>=0.23.0->openai) (0.14.0)\n", + "Requirement already satisfied: annotated-types>=0.6.0 in /home/andrew/micromamba/envs/ragna-dev/lib/python3.10/site-packages (from pydantic<3,>=1.9.0->openai) (0.7.0)\n", + "Requirement already satisfied: pydantic-core==2.27.2 in /home/andrew/micromamba/envs/ragna-dev/lib/python3.10/site-packages (from pydantic<3,>=1.9.0->openai) (2.27.2)\n", + "Downloading openai-1.59.7-py3-none-any.whl (454 kB)\n", + "Using cached distro-1.9.0-py3-none-any.whl (20 kB)\n", + "Downloading jiter-0.8.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (345 kB)\n", + "Installing collected packages: jiter, distro, openai\n", + "Successfully installed distro-1.9.0 jiter-0.8.2 openai-1.59.7\n" + ] + } + ], + "source": [ + "!pip install openai" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "b7646cdb-fadc-4e57-ad96-05532a211bb5", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"Hello! I'm just a program, so I don't have feelings, but I'm here and ready to help you. How can I assist you today?\"" + ] + }, + "execution_count": 11, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# openai work\n", + "from openai import OpenAI\n", + "\n", + "client = OpenAI()\n", + "def stream_openai(client, role, message):\n", + " stream = client.chat.completions.create(\n", + " model=\"gpt-4o-mini\",\n", + " messages=[{\"role\": role, \"content\": message}],\n", + " stream=True,\n", + " )\n", + " message = \"\"\n", + " for chunk in stream:\n", + " if chunk.choices[0].delta.content is not None:\n", + " message += chunk.choices[0].delta.content\n", + " return message\n", + "\n", + "m = stream_openai(client, \"user\", \"hello, how are you?\")\n", + "m" + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "id": "861713ce-bb5f-484b-b7e3-e96b9b8775ab", + "metadata": {}, + "outputs": [], + "source": [ + "from typing import Optional\n", + "from ragna.source_storages import RagnaDemoSourceStorage\n", + "from ragna.assistants import RagnaDemoAssistant\n", + "from ragna.preprocessors import RagnaDemoPreprocessor\n", + "from ragna import Rag\n", + "from ragna.core import (\n", + " LocalDocument, Document, QueryPreprocessor, MetadataFilter, ProcessedQuery\n", + ")\n", + "from ragna.assistants import Gpt4\n", + "\n", + "\n", + "\n", + "storage = RagnaDemoSourceStorage()\n", + "\n", + "documents = [\n", + " (\n", + " document\n", + " if isinstance(document, Document)\n", + " else LocalDocument.from_path(document)\n", + " )\n", + " for document in md_files\n", + " ]\n", + "storage.store(\"ragna_docs\", documents)\n", + "\n", + "\n", + "class TestPreprocessor(QueryPreprocessor):\n", + "\n", + " def __init__(self):\n", + " # self.storage=storage\n", + " # self.assistant=assistant\n", + " self.messages = []\n", + "\n", + " def ask_assistant(self, prompt):\n", + " instruction = (\"take the following prompt and \"\n", + " \"improve it so that it will be \"\n", + " \"better for querying a database: \" + prompt)\n", + " \n", + " assistant_answer = stream_openai(client, \"user\", instruction)\n", + " return assistant_answer\n", + "\n", + " def process(self, query: str, metadata_filter: Optional[MetadataFilter]):\n", + " processed_query = self.ask_assistant(query)\n", + " return ProcessedQuery(\n", + " original_query=query,\n", + " processed_query=processed_query,\n", + " metadata_filter=None,\n", + " processor_name=self.display_name()\n", + " )\n", + "\n", + "\n", + "chat = Rag().chat(\n", + " input=None,\n", + " source_storage = storage,\n", + " assistant=RagnaDemoAssistant,\n", + " preprocessor=TestPreprocessor,\n", + " corpus_name=\"ragna_docs\"\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 24, + "id": "e0966f19-a524-49ce-9769-75e486231c60", + "metadata": {}, + "outputs": [], + "source": [ + "_ = await chat.prepare()" + ] + }, + { + "cell_type": "code", + "execution_count": 25, + "id": "4a9af3eb-5efe-4d17-875d-5480f928c94d", + "metadata": { + "scrolled": true + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "I'm a demo assistant and can be used to try Ragna's workflow.\n", + "I will only mirror back my inputs. \n", + "\n", + "So far I have received 1 messages.\n", + "\n", + "Your last prompt was:\n", + "\n", + "> To improve the prompt for querying a database, it's important to specify the context, the type of events you're interested in, and any relevant parameters. Here’s a refined version:\n", + "\n", + "\"Can you provide a summary of significant events that occurred in the year 2022, including their dates, locations, and categories (e.g., economic, social, political, or environmental)?\"\n", + "\n", + "This version clarifies the timeframe and the specific details you want to obtain, which will facilitate more accurate database queries.\n", + "\n", + "These are the sources I was given:\n", + "\n", + "- install.md: # Installation ## Prerequisites You need Python 3.9 or above in your working environment to [...]\n", + "- index.md: --- title: Home --- # Ragna — Open source RAG orchestration framework With an intuitive API [...]\n", + "- welcome.md: # Welcome! Thanks for participating in the Ragna community! As an early-stage open source [...]\n", + "- contribute.md: # Contribution guidelines Thanks for your interest in contributing to Ragna! All Ragna [...]\n", + "- what-is-rag.md: # What is RAG? !!! tip \"Under development\" This documentation page is being actively worked [...]\n", + "- README.md: ## Examples\n", + "- python-api.md: # Python API reference ::: ragna.local_root ::: ragna.core ::: ragna.source_storages ::: [...]\n", + "- deploy.md: # REST API reference