diff --git a/examples/conversation_with_RAG_agents/configs/agent_config.json b/examples/conversation_with_RAG_agents/configs/agent_config.json index 5b4ad06b2..56fa21e3b 100644 --- a/examples/conversation_with_RAG_agents/configs/agent_config.json +++ b/examples/conversation_with_RAG_agents/configs/agent_config.json @@ -8,17 +8,23 @@ "model_config_name": "qwen_config", "emb_model_config_name": "qwen_emb_config", "rag_config": { - "load_data": { - "loader": { - "create_object": true, - "module": "llama_index.core", - "class": "SimpleDirectoryReader", - "init_args": { - "input_dir": "../../docs/sphinx_doc/en/source/tutorial", - "required_exts": [".md"] + "index_config": [ + { + "load_data": { + "loader": { + "create_object": true, + "module": "llama_index.core", + "class": "SimpleDirectoryReader", + "init_args": { + "input_dir": "../../docs/sphinx_doc/en/source/tutorial", + "required_exts": [ + ".md" + ] + } + } } } - }, + ], "chunk_size": 2048, "chunk_overlap": 40, "similarity_top_k": 5, @@ -37,41 +43,47 @@ "model_config_name": "qwen_config", "emb_model_config_name": "qwen_emb_config", "rag_config": { - "load_data": { - "loader": { - "create_object": true, - "module": "llama_index.core", - "class": "SimpleDirectoryReader", - "init_args": { - "input_dir": "../../src/agentscope", - "recursive": true, - "required_exts": [".py"] - } - } - }, - "store_and_index": { - "transformations": [ - { - "create_object": true, - "module": "llama_index.core.node_parser", - "class": "CodeSplitter", - "init_args": { - "language": "python", - "chunk_lines": 100 + "index_config": [ + { + "load_data": { + "loader": { + "create_object": true, + "module": "llama_index.core", + "class": "SimpleDirectoryReader", + "init_args": { + "input_dir": "../../src/agentscope", + "recursive": true, + "required_exts": [ + ".py" + ] + } } + }, + "store_and_index": { + "transformations": [ + { + "create_object": true, + "module": "llama_index.core.node_parser", + "class": "CodeSplitter", + "init_args": { + "language": "python", + "chunk_lines": 100 + } + } + ] } - ] - }, + } + ], "chunk_size": 2048, "chunk_overlap": 40, "similarity_top_k": 5, "log_retrieval": false, "recent_n_mem": 1, "persist_dir": "../../rag_storage/code_assist" - } + } } }, - { + { "class": "LlamaIndexAgent", "args": { "name": "API-Assistant", @@ -80,25 +92,31 @@ "model_config_name": "qwen_config", "emb_model_config_name": "qwen_emb_config", "rag_config": { - "load_data": { - "loader": { - "create_object": true, - "module": "llama_index.core", - "class": "SimpleDirectoryReader", - "init_args": { - "input_dir": "../../docs/docstring_html/", - "required_exts": [".html"] + "index_config": [ + { + "load_data": { + "loader": { + "create_object": true, + "module": "llama_index.core", + "class": "SimpleDirectoryReader", + "init_args": { + "input_dir": "../../docs/docstring_html/", + "required_exts": [ + ".html" + ] + } } } - }, - "chunk_size": 2048, - "chunk_overlap": 40, - "similarity_top_k": 3, - "log_retrieval": true, - "recent_n_mem": 1, - "persist_dir": "../../rag_storage/api_assist", - "repo_base": "../../" - } + } + ], + "chunk_size": 2048, + "chunk_overlap": 40, + "similarity_top_k": 3, + "log_retrieval": true, + "recent_n_mem": 1, + "persist_dir": "../../rag_storage/api_assist", + "repo_base": "../../" + } } }, { @@ -106,9 +124,73 @@ "args": { "name": "Agent-Guiding-Assistant", "description": "Agent-Guiding-Assistant is an agent that decide which agent should provide the answer next. It can answer questions about specific functions and classes in AgentScope.", - "sys_prompt": "You're an assistant guiding the user to specific agent for help. The answer is in a cheerful styled language. The output starts with appreciation for the question. Next, rephrase the question in a simple declarative Sentence for example, 'I think you are asking...'. Last, if the question is about detailed code or example in AgentScope Framework, output '@ Code-Search-Assistant you might be suitable for answering the question'; if the question is about API or function calls (Example: 'Is there function related...' or 'how can I initialize ...' ) in AgentScope, output '@ API-Assistant, I think you are more suitable for the question, please tell us more about it'; otherwise, output '@ Tutorial-Assistant, I think you are more suitable for the question, please tell us more about it'. The answer is expected to be one line only", + "sys_prompt": "You're an assistant guiding the user to specific agent for help. The answer is in a cheerful styled language. The output starts with appreciation for the question. Next, rephrase the question in a simple declarative Sentence for example, 'I think you are asking...'. Last, if the question is about detailed code or example in AgentScope Framework, output '@ Code-Search-Assistant you might be suitable for answering the question'; if the question is about API or function calls (Example: 'Is there function related...' or 'how can I initialize ...' ) in AgentScope, output '@ API-Assistant, I think you are more suitable for the question, please tell us more about it'; if question is about where to find some context (Example:'where can I find...'), output '@ Searching-Assistant, we need your help', otherwise, output '@ Tutorial-Assistant, I think you are more suitable for the question, can you tell us more about it?'. The answer is expected to be only one sentence", "model_config_name": "qwen_config", "use_memory": false } + }, +{ + "class": "LlamaIndexAgent", + "args": { + "name": "Searching-Assistant", + "description": "Search-Assistant is an agent that can provide answer based on AgentScope code and tutorial. It can answer questions about everything in AgentScope codes and tutorials.", + "sys_prompt": "You're a helpful assistant of AgentScope. The answer starts with appreciation for the question, then provide output the location of the code or section that the most relevant to the question. The answer is limited to be less than 50 words.", + "model_config_name": "qwen_config", + "emb_model_config_name": "qwen_emb_config", + "rag_config": { + "index_config": [ + { + "load_data": { + "loader": { + "create_object": true, + "module": "llama_index.core", + "class": "SimpleDirectoryReader", + "init_args": { + "input_dir": "../../docs/sphinx_doc/en/source/tutorial", + "required_exts": [ + ".md" + ] + } + } + } + }, + { + "load_data": { + "loader": { + "create_object": true, + "module": "llama_index.core", + "class": "SimpleDirectoryReader", + "init_args": { + "input_dir": "../../src/agentscope", + "recursive": true, + "required_exts": [ + ".py" + ] + } + } + }, + "store_and_index": { + "transformations": [ + { + "create_object": true, + "module": "llama_index.core.node_parser", + "class": "CodeSplitter", + "init_args": { + "language": "python", + "chunk_lines": 100 + } + } + ] + } + } + ], + "chunk_size": 2048, + "chunk_overlap": 40, + "similarity_top_k": 5, + "log_retrieval": false, + "recent_n_mem": 1, + "persist_dir": "../../rag_storage/searching_assist" + } + } } ] \ No newline at end of file diff --git a/examples/conversation_with_RAG_agents/rag/llama_index_rag.py b/examples/conversation_with_RAG_agents/rag/llama_index_rag.py index 7b8026460..8be57d58a 100644 --- a/examples/conversation_with_RAG_agents/rag/llama_index_rag.py +++ b/examples/conversation_with_RAG_agents/rag/llama_index_rag.py @@ -6,6 +6,7 @@ from typing import Any, Optional, List, Union from loguru import logger +import importlib import os.path try: @@ -204,26 +205,27 @@ def load_data( def store_and_index( self, - docs: Any, - vector_store: Union[BasePydanticVectorStore, VectorStore, None] = None, + docs_list: Any, retriever: Optional[BaseRetriever] = None, transformations: Optional[list[NodeParser]] = None, + store_and_index_args_list: Optional[list] = None, **kwargs: Any, ) -> Any: """ Preprocessing the loaded documents. Args: - docs (Any): + docs_list (Any): documents to be processed, usually expected to be in llama index Documents. - vector_store (Union[BasePydanticVectorStore, VectorStore, None]): - vector store in llama index retriever (Optional[BaseRetriever]): optional, specifies the retriever in llama index to be used transformations (Optional[list[NodeParser]]): optional, specifies the transformations (operators) to process documents (e.g., split the documents into smaller chunks) + store_and_index_args_list (Optional[list]): + optional, specifies the indexing configurations in llama + index for each document type Return: Any: return the index of the processed document @@ -231,54 +233,30 @@ def store_and_index( In LlamaIndex terms, an Index is a data structure composed of Document objects, designed to enable querying by an LLM. For example: - 1) preprocessing documents with - 2) generate embedding, - 3) store the embedding-content to vdb + 1) preprocessing documents with data loaders + 2) generate embedding by configuring pipline with embedding models + 3) store the embedding-content to vector database """ - # build and run preprocessing pipeline - if transformations is None: - transformations = [ - SentenceSplitter( - chunk_size=self.config.get( - "chunk_size", - DEFAULT_CHUNK_SIZE, - ), - chunk_overlap=self.config.get( - "chunk_overlap", - DEFAULT_CHUNK_OVERLAP, - ), - ), - ] - - # adding embedding model as the last step of transformation - # https://docs.llamaindex.ai/en/stable/module_guides/loading/ingestion_pipeline/root.html - transformations.append(self.emb_model) + # if persist_dir does not exist, calculate the index if not os.path.exists(self.persist_dir): - # check if index is persisted, if not, calculate the index - if vector_store is None: - # No vector_store is provide, - # use in memory to construct an index - pipeline = IngestionPipeline( - transformations=transformations, - ) - nodes = pipeline.run(documents=docs) - self.index = VectorStoreIndex( - nodes=nodes, - embed_model=self.emb_model, - ) - else: - # use vector_store to construct an index - pipeline = IngestionPipeline( - transformations=transformations, - vector_store=vector_store, - ) - _ = pipeline.run(docs) - self.index = VectorStoreIndex.from_vector_store( - vector_store=vector_store, + # nodes, or called chunks, is a presentation of the documents + nodes = [] + # we build nodes by using the IngestionPipeline for each document + for i in range(len(docs_list)): + nodes = nodes + self.docs_to_nodes( + docs=docs_list[i], + transformations=store_and_index_args_list[i].get( + "transformations", None) ) + + # feed all the nodes to embedding model to calculate index + self.index = VectorStoreIndex( + nodes=nodes, + embed_model=self.emb_model, + ) # persist the calculated index - self.index.storage_context.persist(persist_dir=self.persist_dir) + self.persist_to_dir() else: # load the storage_context storage_context = StorageContext.from_defaults( @@ -307,6 +285,86 @@ def store_and_index( self.retriever = retriever return self.index + def persist_to_dir(self): + """ + Persist the index to the directory. + """ + self.index.storage_context.persist(persist_dir=self.persist_dir) + + def load_docs(self, index_config: dict) -> Any: + """ + Load the documents by configurations. + Args: + index_config (dict): + the index configuration + Return: + Any: the loaded documents + """ + + if "load_data" in index_config: + load_data_args = self._prepare_args_from_config( + index_config["load_data"], + ) + else: + try: + from llama_index.core import SimpleDirectoryReader + except ImportError as exc_inner: + raise ImportError( + " LlamaIndexAgent requires llama-index to be install." + "Please run `pip install llama-index`", + ) from exc_inner + load_data_args = { + "loader": SimpleDirectoryReader( + index_config["set_default_data_path"]), + } + logger.info(f"rag.load_data args: {load_data_args}") + docs = self.load_data(**load_data_args) + return docs + + def docs_to_nodes( + self, + docs: Any, + transformations: Optional[list[NodeParser]] = None + ) -> Any: + """ + Convert the documents to nodes. + Args: + docs (Any): + documents to be processed, usually expected to be in + llama index Documents. + transformations (list[NodeParser]): + specifies the transformations (operators) to + process documents (e.g., split the documents into smaller + chunks) + Return: + Any: return the index of the processed document + """ + # if it is not specified, use the default configuration + if transformations is None: + transformations = [ + SentenceSplitter( + chunk_size=self.config.get( + "chunk_size", + DEFAULT_CHUNK_SIZE, + ), + chunk_overlap=self.config.get( + "chunk_overlap", + DEFAULT_CHUNK_OVERLAP, + ), + ), + ] + # adding embedding model as the last step of transformation + # https://docs.llamaindex.ai/en/stable/module_guides/loading/ingestion_pipeline/root.html + transformations.append(self.emb_model) + + # use in memory to construct an index + pipeline = IngestionPipeline( + transformations=transformations, + ) + # stack up the nodes from the pipline + nodes = pipeline.run(documents=docs) + return nodes + def set_retriever(self, retriever: BaseRetriever) -> None: """ Reset the retriever if necessary. @@ -339,3 +397,59 @@ def retrieve(self, query: str, to_list_strs: bool = False) -> list[Any]: results.append(node.get_text()) return results return retrieved + + def _prepare_args_from_config( + self, + config: dict, + ) -> Any: + """ + Helper function to build args for the two functions: + load_data(...) and store_and_index(docs, ...) + in RAG classes. + Args: + config (dict): a dictionary containing configurations + + Returns: + Any: an object that is parsed/built to be an element + of input to the function of RAG module. + """ + if not isinstance(config, dict): + return config + + if "create_object" in config: + # if a term in args is a object, + # recursively create object with args from config + module_name = config.get("module", "") + class_name = config.get("class", "") + init_args = config.get("init_args", {}) + try: + cur_module = importlib.import_module(module_name) + cur_class = getattr(cur_module, class_name) + init_args = self._prepare_args_from_config(init_args) + logger.info( + f"load and build object{cur_module, cur_class, init_args}", + ) + return cur_class(**init_args) + except ImportError as exc_inner: + logger.error( + f"Fail to load class {class_name} " + f"from module {module_name}", + ) + raise ImportError( + f"Fail to load class {class_name} " + f"from module {module_name}", + ) from exc_inner + else: + prepared_args = {} + for key, value in config.items(): + if isinstance(value, list): + prepared_args[key] = [] + for c in value: + prepared_args[key].append( + self._prepare_args_from_config(c), + ) + elif isinstance(value, dict): + prepared_args[key] = self._prepare_args_from_config(value) + else: + prepared_args[key] = value + return prepared_args diff --git a/examples/conversation_with_RAG_agents/rag_agents.py b/examples/conversation_with_RAG_agents/rag_agents.py index e8b5b5ca2..2de87c96d 100644 --- a/examples/conversation_with_RAG_agents/rag_agents.py +++ b/examples/conversation_with_RAG_agents/rag_agents.py @@ -61,9 +61,8 @@ def __init__( # setup embedding model used in RAG self.emb_model = load_model_by_config_name(emb_model_config_name) + # setup RAG configurations self.rag_config = rag_config or {} - if "log_retrieval" not in self.rag_config: - self.rag_config["log_retrieval"] = True # use LlamaIndexAgent OR LangChainAgent self.rag = self.init_rag() @@ -72,62 +71,6 @@ def __init__( def init_rag(self) -> RAGBase: """initialize RAG with configuration""" - def _prepare_args_from_config( - self, - config: dict, - ) -> Any: - """ - Helper function to build args for the two functions: - rag.load_data(...) and rag.store_and_index(docs, ...) - in RAG classes. - Args: - config (dict): a dictionary containing configurations - - Returns: - Any: an object that is parsed/built to be an element - of input to the function of RAG module. - """ - if not isinstance(config, dict): - return config - - if "create_object" in config: - # if a term in args is a object, - # recursively create object with args from config - module_name = config.get("module", "") - class_name = config.get("class", "") - init_args = config.get("init_args", {}) - try: - cur_module = importlib.import_module(module_name) - cur_class = getattr(cur_module, class_name) - init_args = self._prepare_args_from_config(init_args) - logger.info( - f"load and build object{cur_module, cur_class, init_args}", - ) - return cur_class(**init_args) - except ImportError as exc_inner: - logger.error( - f"Fail to load class {class_name} " - f"from module {module_name}", - ) - raise ImportError( - f"Fail to load class {class_name} " - f"from module {module_name}", - ) from exc_inner - else: - prepared_args = {} - for key, value in config.items(): - if isinstance(value, list): - prepared_args[key] = [] - for c in value: - prepared_args[key].append( - self._prepare_args_from_config(c), - ) - elif isinstance(value, dict): - prepared_args[key] = self._prepare_args_from_config(value) - else: - prepared_args[key] = value - return prepared_args - def reply( self, x: dict = None, @@ -248,38 +191,42 @@ def __init__( An example of the config for retrieving code files is as following: - "rag_config": { - "load_data": { - "loader": { - "create_object": true, - "module": "llama_index.core", - "class": "SimpleDirectoryReader", - "init_args": { - "input_dir": "path/to/data", - "recursive": true - ... - } - } - }, - "store_and_index": { - "transformations": [ - { - "create_object": true, - "module": "llama_index.core.node_parser", - "class": "CodeSplitter", - "init_args": { - "language": "python", - "chunk_lines": 100 + "rag_config":{ + "index_configs": [ + { + "load_data": { + "loader": { + "create_object": true, + "module": "llama_index.core", + "class": "SimpleDirectoryReader", + "init_args": { + "input_dir": "path/to/data", + "recursive": true + ... + } } + }, + "store_and_index": { + "transformations": [ + { + "create_object": true, + "module": "llama_index.core.node_parser", + "class": "CodeSplitter", + "init_args": { + "language": "python", + "chunk_lines": 100 + } + } + ] } - ] - }, + } + ], "chunk_size": 2048, "chunk_overlap": 40, "similarity_top_k": 10, "log_retrieval": true, "recent_n_mem": 1 - } + } """ super().__init__( name=name, @@ -293,45 +240,40 @@ def __init__( def init_rag(self) -> LlamaIndexRAG: # dynamic loading loader - # init rag related attributes + # initiate RAG related attributes rag = LlamaIndexRAG( model=self.model, emb_model=self.emb_model, config=self.rag_config, ) - # load the document to memory - # Feed the AgentScope tutorial documents, so that - # the agent can answer questions related to AgentScope! - if "load_data" in self.rag_config: - load_data_args = self._prepare_args_from_config( - self.rag_config["load_data"], - ) - else: - try: - from llama_index.core import SimpleDirectoryReader - except ImportError as exc_inner: - raise ImportError( - " LlamaIndexAgent requires llama-index to be install." - "Please run `pip install llama-index`", - ) from exc_inner - load_data_args = { - "loader": SimpleDirectoryReader(self.config["data_path"]), - } - # NOTE: "data_path" is never used/defined for the current version. - logger.info(f"rag.load_data args: {load_data_args}") - docs = rag.load_data(**load_data_args) - - # store and indexing - if "store_and_index" in self.rag_config: - store_and_index_args = self._prepare_args_from_config( - self.rag_config["store_and_index"], - ) - else: - store_and_index_args = {} + # initiate the loaded document/store_and_index arguments list, + docs_list, store_and_index_args_list = [], [] + + # load the indexing configurations + index_config = self.rag_config["index_config"] + + # NOTE: as each selected file type may need to use a different loader + # and transformations, the length of the list depends on + # the total count of loaded data. + for index_config_i in range(len(index_config)): + docs = rag.load_docs(index_config = index_config[index_config_i]) + docs_list.append(docs) + + # store and indexing for each file type + if "store_and_index" in index_config[index_config_i]: + store_and_index_args = self._prepare_args_from_config( + index_config[index_config_i]["store_and_index"], + ) + else: + store_and_index_args = {"transformations": None} + store_and_index_args_list.append(store_and_index_args) - logger.info(f"store_and_index_args args: {store_and_index_args}") - rag.store_and_index(docs, **store_and_index_args) + # display the arguments for store_and_index_list + logger.info(f"store_and_index_args args: {store_and_index_args_list}") + # pass the loaded documents and arguments to store_and_index + rag.store_and_index(docs_list=docs_list, + store_and_index_args_list=store_and_index_args_list) return rag def reply( diff --git a/examples/conversation_with_RAG_agents/rag_example.py b/examples/conversation_with_RAG_agents/rag_example.py index 65bf4b61c..c598b9f69 100644 --- a/examples/conversation_with_RAG_agents/rag_example.py +++ b/examples/conversation_with_RAG_agents/rag_example.py @@ -44,24 +44,32 @@ def main() -> None: with open("configs/agent_config.json", "r", encoding="utf-8") as f: agent_configs = json.load(f) + # define RAG-based agents for tutorial and code tutorial_agent = LlamaIndexAgent(**agent_configs[0]["args"]) code_explain_agent = LlamaIndexAgent(**agent_configs[1]["args"]) - # prepare html for api agent + + # NOTE: before defining api-assist, we need to prepare the docstring html + # first prepare_docstring_html( - agent_configs[2]["args"]["rag_config"]["repo_base"], - agent_configs[2]["args"]["rag_config"]["load_data"]["loader"][ - "init_args" - ]["input_dir"], + "../../", + "../../docs/docstring_html/", ) + # define an API agent api_agent = LlamaIndexAgent(**agent_configs[2]["args"]) + # define a guide agent agent_configs[3]["args"].pop("description") guide_agent = DialogAgent(**agent_configs[3]["args"]) + + # define a searching agent + searching_agent = LlamaIndexAgent(**agent_configs[4]["args"]) + rag_agents = [ tutorial_agent, code_explain_agent, api_agent, + searching_agent, ] rag_agent_names = [agent.name for agent in rag_agents] @@ -70,8 +78,8 @@ def main() -> None: # The workflow is the following: # 1. user input a message, # 2. if it mentions one of the agents, then the agent will be called - # 3. otherwise, the guide agent will be decide which agent to call - # 4. the called agent will response to the user + # 3. otherwise, the guide agent will decide which agent to call + # 4. the called agent will respond to the user # 5. repeat x = user_agent() x.role = "user" # to enforce dashscope requirement on roles