├── .env ├── ImportData.py ├── LICENSE.md ├── README.md ├── TalkToDatabase.py ├── TalkToOllama.py ├── TalkToOpenAI.py ├── chainlit.md ├── data ├── GA-2-3-7-swagger-v1.annotated.json ├── b_cisco_catalyst_center_user_guide_237.pdf └── extended_apispecs_documentation.json ├── images ├── architecture.png ├── example_clients_summary.png ├── example_eos.png ├── example_spreadsheet.png ├── inferencing.png ├── preparing-data.png └── youtube-thumbnail.png ├── main.py └── pyproject.toml /.env: -------------------------------------------------------------------------------- 1 | OPENAI_API_KEY= -------------------------------------------------------------------------------- /ImportData.py: -------------------------------------------------------------------------------- 1 | """ 2 | Cisco Sample Code License 1.1 3 | Author: flopach 2024 4 | """ 5 | import json 6 | import glob 7 | import fitz 8 | from bs4 import BeautifulSoup 9 | import requests 10 | import logging 11 | log = logging.getLogger("applogger") 12 | 13 | class DataHandler: 14 | def __init__(self, database, LLM): 15 | self.llm = LLM 16 | self.database = database 17 | 18 | def scrape_pdfuserguide_catcenter(self,filepath): 19 | """ 20 | Scrape Catalyst Center PDF User Guide 21 | """ 22 | try: 23 | log.info(f"=== Start: Chunking + embedding PDF User Guide file ===") 24 | with fitz.open(filepath) as doc: # open document 25 | content = chr(12).join([page.get_text() for page in doc]) 26 | 27 | chunks = self._chunk_text(content,512) 28 | 29 | self.database.collection_add( 30 | documents=chunks, 31 | ids=[f"user_guide_{x}" for x in range(len(chunks))] 32 | ) 33 | log.info(f"=== End: Chunking + embedding PDF User Guide file ===") 34 | except Exception as e: 35 | log.error(f"Error when reading PDF! Error: {e}") 36 | 37 | def scrape_apidocs_catcenter(self): 38 | """ 39 | Scrape developer.cisco.com Catalyst Center API docs 40 | """ 41 | base_url = "https://developer.cisco.com/docs/dna-center/" 42 | 43 | docs_list = [ 44 | "overview", 45 | "getting-started", 46 | "api-quick-start", 47 | "asynchronous-apis", 48 | "authentication-and-authorization", 49 | "command-runner", 50 | "credentials", 51 | "device-onboarding", 52 | "device-provisioning", 53 | "devices", 54 | "discovery", 55 | "events", 56 | "global-ip-pool", 57 | "health-monitoring", 58 | "path-trace", 59 | "rma-device-replacement", 60 | "reports", 61 | "software-defined-access-sda", 62 | "sites", 63 | "swim", 64 | "topology" 65 | ] 66 | 67 | for doc in docs_list: 68 | try: 69 | r = requests.get(base_url+doc) 70 | soup = BeautifulSoup(r.content, 'html.parser') 71 | chunks = self._chunk_text(soup.get_text(),512) 72 | 73 | #log.info(chunks) 74 | 75 | self.database.collection_add( 76 | documents=chunks, 77 | ids=[f"{doc}_{x}" for x in range(len(chunks))], 78 | metadatas=[{ "doc_type" : "apidocs" } for x in range(len(chunks))] 79 | ) 80 | 81 | log.info(f"Scraped data from {base_url+doc}") 82 | 83 | except Exception as e: 84 | log.error(f"Error when requesting data from {base_url+doc}! Error: {e}") 85 | 86 | log.info(f"=== Done with api docs scraping ===") 87 | 88 | def import_apispecs_from_json(self): 89 | """ 90 | This function is used to embed the already existing EXTENDED API specification. The data was generated with GPT-3.5-turbo. 91 | 92 | The function import_apispecs_generate_new_data() is doing the full implementation: Extend the data + embed (see below) 93 | """ 94 | # open openAPI specs file 95 | with open("data/extended_apispecs_documentation.json", "r") as f: 96 | dict = json.load(f) 97 | log.info(f"=== Opened EXTENDED API Specification ===") 98 | 99 | total_num = len(dict["documents"]) 100 | 101 | # zipping together all 3 arrays from the JSON file + iterating 102 | for i, (j_document, j_id, j_metadatas) in enumerate(zip(dict["documents"],dict["ids"],dict["metadatas"])): 103 | # logging status 104 | log.info(f"Working on {i} out of {total_num} ({j_id}).") 105 | 106 | document_chunks = self._chunk_text(j_document,512) 107 | 108 | # create for each document chunk ids. Use operationId as base id. 109 | ids = [f"{j_id}_{x}" for x in range(len(document_chunks))] 110 | 111 | # create metadata for each document chunk 112 | metadatas = [j_metadatas for x in range(len(document_chunks))] 113 | 114 | #logging chunks 115 | #log.debug(document_chunks) 116 | 117 | # === put all information into vectorDB === 118 | 119 | # add into vectordb 120 | self.database.collection_add( 121 | documents=document_chunks, 122 | ids=ids, 123 | metadatas=metadatas 124 | ) 125 | 126 | log.info(f"=== Chunked and embedded the openapi specification into the vectorDB ===") 127 | 128 | def import_apispecs_generate_new_data(self,filepath): 129 | """ 130 | The existing API specification will be extended with the LLM in the function: import_apispecs_generate_new_data() 131 | 132 | 1. Only specific data is extracted from the OpenAPI document 133 | 2. Based on the information within the vectorDB (API docs, User Guide) an extended description is created via the LLM 134 | 3. The newly created information is saved in the vectorDB + external JSON document 135 | 136 | Args: 137 | filepath (str): path to file 138 | """ 139 | 140 | json_document = { 141 | "documents" : [], 142 | "ids" : [], 143 | "metadatas" : [] 144 | } 145 | 146 | # open openAPI specs file 147 | with open(filepath, "r") as f: 148 | dict = json.load(f) 149 | log.info(f"=== Opened API Specification ===") 150 | 151 | p = 1 152 | for path in dict["paths"]: 153 | """ loop through each API path in the document """ 154 | path_dict = dict["paths"][path] 155 | log.info(f'=== STATUS: {p} out of {len(dict["paths"])} paths ===') 156 | p += 1 157 | 158 | for operation in path_dict: 159 | """ loop through each REST operation """ 160 | summary = path_dict[operation]["summary"] 161 | operationId = path_dict[operation]["operationId"] 162 | description = path_dict[operation]["description"] 163 | first_tag = path_dict[operation]["tags"][0] 164 | 165 | # if parameters are defined, list them 166 | if len(path_dict[operation]["parameters"]) != 0: 167 | parameters = "" 168 | for parameter in path_dict[operation]["parameters"]: 169 | """ loop through each parameters """ 170 | p_name = parameter["name"] 171 | p_description = parameter["description"] 172 | 173 | p_in = f'The query parameters should be used in the {parameter["in"]}. ' 174 | 175 | p_default_value = "" 176 | if "default" in parameter: 177 | if parameter["default"] != "": 178 | p_default_value = f'The default value is "{parameter["default"]}". ' 179 | 180 | p_required = "" 181 | if "required" in parameter: 182 | p_required = "This query parameter is required. " 183 | else: 184 | p_required = "This query parameter is not required. " 185 | 186 | parameters += f"- {p_name}: {p_description}. {p_in}{p_default_value}{p_required}\n" 187 | parameters = f"REST API query parameters:\n{parameters}\n" 188 | else: 189 | parameters = "" 190 | 191 | # Generate extended description for this API Call 192 | ai_description = self.llm.extend_api_description(f"{summary}.{description}",path,operation,parameters) 193 | 194 | # === Assemble all information === 195 | 196 | # Create for each API path an extended documentation 197 | # chunk the document into several parts 198 | content = f"""{ai_description}\n\nREST API query information delimited with XML tags\n\nAPI query path:{path}\nREST operation:{operation}\n{parameters}""" 199 | document_chunks = self._chunk_text(content,512) 200 | 201 | # create for each document chunk ids. Use operationId as base id. 202 | ids = [f"{operationId}_{x}" for x in range(len(document_chunks))] 203 | 204 | # create metadata for each document chunk 205 | metadatas = [{ "summary": summary, "tag" : first_tag, "doc_type" : "apispecs" } for x in range(len(document_chunks))] 206 | 207 | #logging chunks 208 | #log.debug(document_chunks) 209 | 210 | # === put all information into vectorDB === 211 | 212 | # add into vectordb 213 | self.database.collection_add( 214 | documents=document_chunks, 215 | ids=ids, 216 | metadatas=metadatas 217 | ) 218 | 219 | # === put all information into a dict which will be saved later to JSON === 220 | 221 | json_document["documents"].append(content) 222 | json_document["ids"].append(operationId) 223 | json_document["metadatas"].append({ "summary": summary, "tag" : first_tag, "doc_type" : "apispecs" }) 224 | 225 | log.info(f"=== NEW document added:\n{content} ===") 226 | 227 | # === put all information into JSON (optional, plain-text saving) === 228 | 229 | with open("data/extended_apispecs_documentation.json", "w") as f: 230 | json.dump(json_document, f) 231 | 232 | log.info(f"=== Extended, chunked, embedded the openapi specification into the vectorDB ===") 233 | 234 | def _chunk_text(self,content,chunk_size): 235 | """ 236 | The most basic method: Chunking by characters 237 | + Replacing the new line character with a white space 238 | + Removing any leading and trailing whitespaces 239 | 240 | Args: 241 | content (str): string to chunk 242 | chunk_size (int): number of characters when to cut 243 | """ 244 | chunks = [content[i:i + chunk_size].replace("\n"," ").strip() for i in range(0, len(content), chunk_size)] 245 | return chunks -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | CISCO SAMPLE CODE LICENSE 2 | Version 1.1 3 | Copyright (c) 2024 Cisco and/or its affiliates 4 | 5 | These terms govern this Cisco Systems, Inc. ("Cisco"), example or demo 6 | source code and its associated documentation (together, the "Sample 7 | Code"). By downloading, copying, modifying, compiling, or redistributing 8 | the Sample Code, you accept and agree to be bound by the following terms 9 | and conditions (the "License"). If you are accepting the License on 10 | behalf of an entity, you represent that you have the authority to do so 11 | (either you or the entity, "you"). Sample Code is not supported by Cisco 12 | TAC and is not tested for quality or performance. This is your only 13 | license to the Sample Code and all rights not expressly granted are 14 | reserved. 15 | 16 | 1. LICENSE GRANT: Subject to the terms and conditions of this License, 17 | Cisco hereby grants to you a perpetual, worldwide, non-exclusive, non- 18 | transferable, non-sublicensable, royalty-free license to copy and 19 | modify the Sample Code in source code form, and compile and 20 | redistribute the Sample Code in binary/object code or other executable 21 | forms, in whole or in part, solely for use with Cisco products and 22 | services. For interpreted languages like Java and Python, the 23 | executable form of the software may include source code and 24 | compilation is not required. 25 | 26 | 2. CONDITIONS: You shall not use the Sample Code independent of, or to 27 | replicate or compete with, a Cisco product or service. Cisco products 28 | and services are licensed under their own separate terms and you shall 29 | not use the Sample Code in any way that violates or is inconsistent 30 | with those terms (for more information, please visit: 31 | www.cisco.com/go/terms). 32 | 33 | 3. OWNERSHIP: Cisco retains sole and exclusive ownership of the Sample 34 | Code, including all intellectual property rights therein, except with 35 | respect to any third-party material that may be used in or by the 36 | Sample Code. Any such third-party material is licensed under its own 37 | separate terms (such as an open source license) and all use must be in 38 | full accordance with the applicable license. This License does not 39 | grant you permission to use any trade names, trademarks, service 40 | marks, or product names of Cisco. If you provide any feedback to Cisco 41 | regarding the Sample Code, you agree that Cisco, its partners, and its 42 | customers shall be free to use and incorporate such feedback into the 43 | Sample Code, and Cisco products and services, for any purpose, and 44 | without restriction, payment, or additional consideration of any kind. 45 | If you initiate or participate in any litigation against Cisco, its 46 | partners, or its customers (including cross-claims and counter-claims) 47 | alleging that the Sample Code and/or its use infringe any patent, 48 | copyright, or other intellectual property right, then all rights 49 | granted to you under this License shall terminate immediately without 50 | notice. 51 | 52 | 4. LIMITATION OF LIABILITY: CISCO SHALL HAVE NO LIABILITY IN CONNECTION 53 | WITH OR RELATING TO THIS LICENSE OR USE OF THE SAMPLE CODE, FOR 54 | DAMAGES OF ANY KIND, INCLUDING BUT NOT LIMITED TO DIRECT, INCIDENTAL, 55 | AND CONSEQUENTIAL DAMAGES, OR FOR ANY LOSS OF USE, DATA, INFORMATION, 56 | PROFITS, BUSINESS, OR GOODWILL, HOWEVER CAUSED, EVEN IF ADVISED OF THE 57 | POSSIBILITY OF SUCH DAMAGES. 58 | 59 | 5. DISCLAIMER OF WARRANTY: SAMPLE CODE IS INTENDED FOR EXAMPLE PURPOSES 60 | ONLY AND IS PROVIDED BY CISCO "AS IS" WITH ALL FAULTS AND WITHOUT 61 | WARRANTY OR SUPPORT OF ANY KIND. TO THE MAXIMUM EXTENT PERMITTED BY 62 | LAW, ALL EXPRESS AND IMPLIED CONDITIONS, REPRESENTATIONS, AND 63 | WARRANTIES INCLUDING, WITHOUT LIMITATION, ANY IMPLIED WARRANTY OR 64 | CONDITION OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, NON- 65 | INFRINGEMENT, SATISFACTORY QUALITY, NON-INTERFERENCE, AND ACCURACY, 66 | ARE HEREBY EXCLUDED AND EXPRESSLY DISCLAIMED BY CISCO. CISCO DOES NOT 67 | WARRANT THAT THE SAMPLE CODE IS SUITABLE FOR PRODUCTION OR COMMERCIAL 68 | USE, WILL OPERATE PROPERLY, IS ACCURATE OR COMPLETE, OR IS WITHOUT 69 | ERROR OR DEFECT. 70 | 71 | 6. GENERAL: This License shall be governed by and interpreted in 72 | accordance with the laws of the State of California, excluding its 73 | conflict of laws provisions. You agree to comply with all applicable 74 | United States export laws, rules, and regulations. If any provision of 75 | this License is judged illegal, invalid, or otherwise unenforceable, 76 | that provision shall be severed and the rest of the License shall 77 | remain in full force and effect. No failure by Cisco to enforce any of 78 | its rights related to the Sample Code or to a breach of this License 79 | in a particular situation will act as a waiver of such rights. In the 80 | event of any inconsistencies with any other terms, this License shall 81 | take precedence. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Create your own API Assistant with Cisco Catalyst Center 2 | 3 | **Generate Python code around the REST API of Cisco Catalyst Center** (formerly Cisco DNA Center). You can blueprint this project for any other REST API. 4 | 5 | ![](images/architecture.png) 6 | 7 | **Features**: 8 | 9 | * **Learn & understand RAG**: Nicely structured & well documented code 10 | * Python libraries are **kept to a minimum** (no LangChain, no Llamaindex) 11 | * **OpenAI APIs and Open Source LLMs** are supported. 12 | * Use it to **save time when creating new code** for Catalyst Center APIs 13 | * Fully functioning **full-stack application** with front- & back-end 14 | 15 | **Check out the [YouTube Video](https://youtu.be/7U1LZU2jNyw)!** 16 | 17 | [![](images/youtube-thumbnail.png)](https://youtu.be/7U1LZU2jNyw) 18 | 19 | 20 | ## Output Examples 21 | 22 | ### Example 1 23 | 24 | **User query: "get me a list of all end of sales devices. include authentication"** 25 | 26 | ![](images/example_eos.png) 27 | 28 | --- 29 | 30 | ### Example 2 31 | 32 | **User query: "export a summary of all clients"** 33 | 34 | ![](images/example_clients_summary.png) 35 | 36 | --- 37 | 38 | ### Example 3 39 | 40 | **User query: "get me a list of all devices and export this list as a spreadsheet locally. include the authentication function"** 41 | 42 | ![](images/example_spreadsheet.png) 43 | 44 | --- 45 | 46 | ### Other input examples 47 | 48 | * export all templates 49 | * run the cli command "show version" directly on one specific device. include authentication 50 | * get the latest running configuration of one device 51 | * how to claim a device to a site 52 | 53 | ## Getting Started 54 | 55 | ### 1. Download & Install 56 | 57 | Clone the code & install all required libraries (recommended with [Poetry](https://pypi.org/project/poetry/)): 58 | 59 | At first install poetry on your computer: 60 | 61 | ``` 62 | pip install poetry 63 | ``` 64 | 65 | Then, create all dependencies within a virtual environment (using poetry): 66 | 67 | ``` 68 | git clone https://github.com/flopach/create-your-own-api-assistant-cisco-catalyst-center && 69 | cd create-your-own-api-assistant-cisco-catalyst-center && 70 | poetry install && 71 | poetry shell 72 | ``` 73 | 74 | ### 2. Decide which LLM to use 75 | 76 | Decide which LLM to use: OpenAI or Open-Source LLM with Ollama 77 | 78 | * **OpenAI**: If not already done for, insert your OpenAI API key as stated in the [OpenAI documentation](https://platform.openai.com/docs/quickstart/step-2-set-up-your-api-key). Simply add your OpenAI API key in the `.env`file. 79 | * **Open Source LLM**: In order to run your LLM locally, install [Ollama](https://ollama.com/) and download the LLM of your choice (e.g. llama3). 80 | 81 | Open **main.py** and change the parameters if needed. Default settings are: OpenAI with the model "gpt-3.5-turbo". 82 | 83 | ### 3. Start the server 84 | 85 | Run the server and start chatting at [http://localhost:8000/](http://localhost:8000/): 86 | 87 | ``` 88 | chainlit run main.py 89 | ``` 90 | 91 | ### 4. Import data (first run only) 92 | 93 | You should see a chat window. **If it is your first run**, type `importdata` to load, chunk and embed all data in the vector database. 94 | 95 | > **Note**: Depending on the LLM, settings and config this can take some time. You can see the current status in the terminal. 96 | > 97 | > **Example**: Using llama3 with no full data import on a Macbook Pro M1 (16GB RAM) took around 10 minutes. 98 | 99 | ## Architecture & Components 100 | 101 | The app follows the RAG architecture (Retrieval Augmented Generation). This is an efficient approach where relevant context data will be added to the LLM-query of the user. 102 | 103 | ![](images/architecture.png) 104 | 105 | Components overview: 106 | 107 | * **Data Layer**: data to be stored in the vector database 108 | * Catalyst Center User Guide, 900 pages PDF document 109 | * Catalyst Center API documentation, scraped from [developer.cisco.com/docs/dna-center/](https://developer.cisco.com/docs/dna-center/) 110 | * Catalyst Center OpenAPI Specification. This documentation will be extended with newly created data by the LLM. 111 | * **Application Components**: 112 | * [Vector Database ChromaDB](https://www.trychroma.com/): an easy-to-use vector database with a well-documented Python SDK 113 | * LLM: OpenAI GPT APIs or local Open Source LLM (e.g. llama3) with Ollama 114 | * web UI for the browser conversation: [Chainlit Python library](https://chainlit.io) 115 | * **Python Code Structure**: 116 | * **main.py** - Starting point of the app. This is where all class instances are created and the webUI via chainlit is defined. 117 | * **ImportData.py** - The listed data above is being imported in the class _DataHandler_. 118 | * **TalkToDatabase.py** - The database interaction (querying, embedding data) is done through the class _vectorDB_ 119 | * **TalkToOpenAI.py** - Used for the interactions with OpenAI's GPT via their REST APIs. 120 | * **TalkToOllama.py** - Used for the interactions with Ollama. 121 | 122 | ## RAG: Preparing data (ImportData.py) 123 | 124 | In a RAG architecture, you embed your own (local) data into a vector database. When the user is asking the LLM to generate data (= LLM-inferencing), some context data is also sent together with the user-query to the LLM to generate output. 125 | 126 | ![](images/preparing-data.png) 127 | 128 | 3 different examples of how to import and pre-process the data: 129 | 130 | * **Import PDF document (user guide)**: Only the text of the 900 pages user guide of the Catalyst Center will be exported and based on a fixed number of characters splitted. Then, the chunks will be converted to vectors with a pre-defined embedding function and inserted into the vector database. 131 | * **Scraping websites (API documenation)**: Since the API documentation is located at [developer.cisco.com/docs/dna-center/](https://developer.cisco.com/docs/dna-center/), the documentation will be requested and text data will be scraped from the HTML documents. Then the text will be chunked, embedded and inserted. 132 | * **Generating new content based on existing data (API Specification)**: Since the API specification contains all the REST API calls, it is very important to prepare this document thoughtfully. Therefore, only the non-redundant information are getting extracted and the API query descriptions are extended with the LLM based on existing knowledge stored in the vector database. Use-cases are also included. 133 | 134 | > **Note**: Generating new data with the API specification can be time intense and is therefore optional per default. It takes approximately 1 hour with OpenAI APIs (GPT-3.5-turbo) and around 10 hours with llama3-8B on a Macbook Pro M1 (16GB RAM). 135 | > 136 | > That's why I have already included the generated data in a JSON file `extended_apispecs_documentation.json` located in the `/data` folder. This data is generated with GPT-3.5-turbo. 137 | 138 | ## RAG: Inferencing 139 | 140 | Once the data is imported, the user can query the LLM. 141 | 142 | ![](images/inferencing.png) 143 | 144 | 1. User is writing the task into the chat 145 | 2. The task (string) is vectorized and queried against the vector database. The vector database returns a specific number of documents which are semantically similar to the task. 146 | 3. The task string and the context information is put into the prompt of the LLM. 147 | 4. Finally, the LLM is giving the output based on the provided data input. 148 | 149 | ## Next steps & FAQs 150 | 151 | **Q: Is this app ready for production? Is providing 100% accurate results?** 152 | 153 | No. This project can be useful especially when developing new applications with the Catalyst Center REST API, but the output might not be 100% correct. 154 | 155 | **Q: How can the LLM generate better results?** 156 | 157 | Try out other chunking methods, add other relevant data, extend the system prompt, include the response schemas from the openAPI specifications, etc. There are many ways you can tweak this app even more to generate better results! 158 | 159 | **Q: Should I use OpenAI or Ollama? What is your experience?** 160 | 161 | Try them both! You will see different performances for each LLM. I got better results with GPT-3.5.turbo compared to llama3-8B. 162 | 163 | **Q: How can I test the Python code if I don't have access to a Cisco Catalyst Center?** 164 | 165 | You can use a DevNet sandbox for free. Use the [Catalyst Center always-on sandbox](https://devnetsandbox.cisco.com/DevNet/catalog/Catalyst-Center-Always-On): Copy the URL + credentials into your script. 166 | 167 | **Q: How can I change the bot name and auto-collapse messages?** 168 | 169 | Some chainlit settings need to be set in the configuration file which can not be changed during runtime. Therefore, only the [default parameters are used](https://docs.chainlit.io/backend/config/ui). 170 | 171 | You can change these settings in **.chainlit/config.toml**: 172 | 173 | ``` 174 | [UI] 175 | # Name of the app and chatbot. 176 | name = "Catalyst Center API+Code Assistant" 177 | 178 | # Large size content are by default collapsed for a cleaner ui 179 | default_collapse_content = false 180 | ``` 181 | 182 | ## Versioning 183 | 184 | **1.0** - initial version. RAG with Catalyst Center 2.3.7 185 | 186 | ## Authors 187 | 188 | * **Flo Pachinger** - *Initial work* - [flopach](https://github.com/flopach) 189 | 190 | ## License 191 | 192 | This project is licensed under the Cisco Sample Code License 1.1 - see the [LICENSE.md](LICENSE.md) file for details. 193 | 194 | The Cisco Catalyst User Guide (PDF document) located in the "data" folder is copyright by Cisco. 195 | 196 | ## Further Links 197 | 198 | * [Cisco DevNet Website](https://developer.cisco.com) 199 | -------------------------------------------------------------------------------- /TalkToDatabase.py: -------------------------------------------------------------------------------- 1 | """ 2 | Cisco Sample Code License 1.1 3 | Author: flopach 2024 4 | """ 5 | import chromadb 6 | import os 7 | import logging 8 | log = logging.getLogger("applogger") 9 | 10 | class VectorDB: 11 | def __init__(self, collection_name, embeddings_function = "openai", database_path = "chromadb/"): 12 | """ 13 | Create new VectorDB instance 14 | 15 | Args: 16 | collection_name (str): Name of the collectiong 17 | embeddings_function (str): "openai" or "ollama" 18 | database_path (str): persistent storage for vectorDB 19 | """ 20 | 21 | # define chromadb client 22 | self.chromadb_client = chromadb.PersistentClient(path=database_path) 23 | 24 | # set embeddings function 25 | # different for each chosen LLM 26 | if embeddings_function == "openai": 27 | self.embeddings_function = chromadb.utils.embedding_functions.OpenAIEmbeddingFunction( 28 | api_key=os.getenv('OPENAI_API_KEY'), 29 | model_name="text-embedding-3-small" 30 | ) 31 | elif embeddings_function == "ollama": 32 | self.embeddings_function = chromadb.utils.embedding_functions.DefaultEmbeddingFunction() 33 | 34 | # set collection 35 | self.collection = self.chromadb_client.get_or_create_collection(name=collection_name,embedding_function=self.embeddings_function) 36 | 37 | def query_db(self, query_string, n_results, where_clause=None): 38 | """ 39 | Query the vector DB 40 | 41 | Args: 42 | query_string (str): specific query string 43 | n_results 44 | where_clause (str): Option to define WHERE clause for vectorDB query: 45 | default --> None 46 | apidocs --> {"doc_type": "apidocs"} 47 | apispecs --> {"doc_type": "apispecs"} 48 | """ 49 | 50 | # define vectorDB search 51 | # docs: https://docs.trychroma.com/reference/Collection#query 52 | 53 | if where_clause == "apidocs": 54 | where_clause = {"doc_type": "apidocs"} 55 | elif where_clause == "apispecs": 56 | where_clause = {"doc_type": "apispecs"} 57 | 58 | results = self.collection.query( 59 | query_texts=[query_string], 60 | n_results=n_results, 61 | where=where_clause 62 | ) 63 | 64 | # Display queried documents 65 | log.debug(f'Queried documents: {results["metadatas"]}') 66 | log.debug(f'Queried distances: {results["distances"]}') 67 | 68 | return results["documents"] 69 | 70 | def collection_add(self,documents,ids,metadatas=None): 71 | """ 72 | Add to collection 73 | 74 | Args: 75 | documents (dict): list of chunked documents 76 | ids (dict): list of IDs 77 | metadatas (dict): list of metadata 78 | """ 79 | r = self.collection.add( 80 | documents=documents, 81 | ids=ids, 82 | metadatas=metadatas, 83 | ) 84 | if r != None: 85 | log.warning(f"{ids} returned NOT None...") -------------------------------------------------------------------------------- /TalkToOllama.py: -------------------------------------------------------------------------------- 1 | """ 2 | Cisco Sample Code License 1.1 3 | Author: flopach 2024 4 | """ 5 | import ollama 6 | import time 7 | import logging 8 | log = logging.getLogger("applogger") 9 | 10 | class LLMOllama: 11 | def __init__(self, database, model = "llama3"): 12 | self.database = database 13 | self.model = model 14 | 15 | def extend_api_description(self,query_string,path,operation,parameters): 16 | """ 17 | Extend the description for each API REST Call operation 18 | 19 | Args: 20 | query_string (str): details of the REST API call 21 | path (str): REST API path 22 | operation (str): REST API operation (GET, POST, etc.) 23 | parameters (str): Query parameters 24 | """ 25 | 26 | # query vector DB for local data 27 | # query without the where_clause to include context from the User Guide PDF 28 | context_query = self.database.query_db(query_string,20) 29 | 30 | # create promt message with local context data 31 | message = f'Query path: "{path}"\nREST operation: {operation}\nshort description: {query_string}\n{parameters}\nUse this context delimited with XML tags:\n\n{context_query}\n' 32 | log.debug(f"===== Extending the description with:\n {message}") 33 | 34 | # ask LLM 35 | response = ollama.chat(model=self.model, messages=[ 36 | {"role": "system", 37 | "content": "You are provided information of a specific REST API query path of the Cisco Catalyst Center. Describe what this query is for. Describe how this query can be used from a user perspective."}, 38 | {"role": "user", 39 | "content": message} 40 | ] 41 | ) 42 | 43 | return response['message']['content'] 44 | 45 | def ask_llm(self,query_string,n_results_apidocs=10,n_results_apispecs=20): 46 | """ 47 | Ask the LLM with the query string. 48 | Search for context in vectorDB 49 | 50 | Args: 51 | query_string (str): details of the REST API call 52 | n_results_apidocs (int): Number of documents return by vectorDB query for API docs on developer.cisco.com 53 | n_results_apispecs (int): Number of documents return by vectorDB query for extended API specification document 54 | """ 55 | # Record the start time 56 | start_time = time.time() 57 | 58 | # context queries to vectorDB 59 | context_query_apidocs = self.database.query_db(query_string,n_results_apidocs,"apidocs") 60 | context_query_apispecs = self.database.query_db(query_string,n_results_apispecs,"apispecs") 61 | context = f'''Context information delimited with XML tags:\n\n{context_query_apidocs}\n 62 | API specification context delimited with XML tags:\n\n{context_query_apispecs}\n''' 63 | 64 | question = f"\n\nUser question: '{query_string}'" 65 | 66 | # assemble message for LLM with context + user question 67 | message = context + question 68 | 69 | log.debug(message) 70 | 71 | response = ollama.chat(model=self.model, messages=[ 72 | { 73 | "role": "system", 74 | "content": """You are the Cisco Catalyst Center REST API and Python code assistant. You provide documentation and Python code for developers. 75 | Always list all available query parameters from the provided context. Include the REST operation and query path. 76 | 1. you create documentation to the specific API calls. 77 | 2. you create an example source code in the programming language Python using the 'requests' library. 78 | Tell the user if you do not know the answer. If loops or advanced code is needed, provide it. 79 | ### 80 | Every API query needs to include the header parameter 'X-Auth-Token' for authentication and authorization. This is where the access token is defined. 81 | If the user does not have the access token, the user needs to call the REST API query '/dna/system/api/v1/auth/token' to receive the access token. Only the API query '/dna/system/api/v1/auth/token' is using the Basic authentication scheme, as defined in RFC 7617. All other API queries need to have the header parameter 'X-Auth-Token' defined. 82 | ### 83 | """ 84 | },{ 85 | 'role': 'user', 86 | 'content': message, 87 | } 88 | ]) 89 | 90 | # Calculate the total duration 91 | duration = round(time.time() - start_time, 2) 92 | exec_duration = f"The query '{query_string}' took **{duration} seconds** to execute." 93 | log.info(exec_duration) 94 | 95 | return response['message']['content']+"\n\n"+exec_duration -------------------------------------------------------------------------------- /TalkToOpenAI.py: -------------------------------------------------------------------------------- 1 | """ 2 | Cisco Sample Code License 1.1 3 | Author: flopach 2024 4 | """ 5 | from openai import OpenAI 6 | import time 7 | import logging 8 | log = logging.getLogger("applogger") 9 | 10 | class LLMOpenAI: 11 | def __init__(self, database, model = "gpt-3.5-turbo"): 12 | self.client = OpenAI() 13 | self.database = database 14 | self.model = model 15 | 16 | def extend_api_description(self,query_string,path,operation,parameters): 17 | """ 18 | Extend the description for each API REST Call operation 19 | 20 | Args: 21 | query_string (str): details of the REST API call 22 | path (str): REST API path 23 | operation (str): REST API operation (GET, POST, etc.) 24 | parameters (str): Query parameters 25 | """ 26 | 27 | # query vector DB for local data 28 | context_query = self.database.query_db(query_string,10) 29 | 30 | # create promt message with local context data 31 | message = f'Query path: "{path}"\nREST operation: {operation}\nshort description: {query_string}\n{parameters}\nUse this context delimited with XML tags:\n\n{context_query}\n' 32 | log.debug(f"=== Extending the description with: ===\n {message}") 33 | 34 | # ask GPT 35 | completion = self.client.chat.completions.create( 36 | model=self.model, 37 | temperature=0.8, 38 | messages=[ 39 | {"role": "system", "content": "You are provided information of a specific REST API query path of the Cisco Catalyst Center. Describe what this query is for in detail. Describe how this query can be used from a user perspective."}, 40 | {"role": "user", "content": message} 41 | ] 42 | ) 43 | 44 | return completion.choices[0].message.content 45 | 46 | def ask_llm(self,query_string,n_results_apidocs=10,n_results_apispecs=20): 47 | """ 48 | Ask the LLM with the query string. 49 | Search for context in vectorDB 50 | 51 | Args: 52 | query_string (str): details of the REST API call 53 | n_results_apidocs (int): Number of documents return by vectorDB query for API docs on developer.cisco.com 54 | n_results_apispecs (int): Number of documents return by vectorDB query for extended API specification document 55 | """ 56 | # Record the start time 57 | start_time = time.time() 58 | 59 | # context queries to vectorDB 60 | context_query_apidocs = self.database.query_db(query_string,n_results_apidocs,"apidocs") 61 | context_query_apispecs = self.database.query_db(query_string,n_results_apispecs,"apispecs") 62 | context = f'''Context information delimited with XML tags:\n\n{context_query_apidocs}\n 63 | API specification context delimited with XML tags:\n\n{context_query_apispecs}\n''' 64 | 65 | question = f"\n\nUser question: '{query_string}'" 66 | 67 | message = context + question 68 | 69 | log.debug(message) 70 | 71 | completion = self.client.chat.completions.create( 72 | model=self.model, 73 | temperature=0.8, 74 | messages=[ 75 | { "role": "system", 76 | "content": """You are the Cisco Catalyst Center REST API and Python code assistant. You provide documentation and Python code for developers. 77 | Always list all available query parameters from the provided context. Include the REST operation and query path. 78 | 1. you create documentation to the specific API calls. 79 | 2. you create an example source code in the programming language Python using the 'requests' library. 80 | Tell the user if you do not know the answer. If loops or advanced code is needed, provide it. 81 | ### 82 | Every API query needs to include the header parameter 'X-Auth-Token' for authentication and authorization. This is where the access token is defined. 83 | If the user does not have the access token, the user needs to call the REST API query '/dna/system/api/v1/auth/token' to receive the access token. Only the API query '/dna/system/api/v1/auth/token' is using the Basic authentication scheme, as defined in RFC 7617. All other API queries need to have the header parameter 'X-Auth-Token' defined. 84 | ### 85 | """ 86 | }, 87 | {"role": "user", "content": message} 88 | ] 89 | ) 90 | 91 | # Calculate the total duration 92 | duration = round(time.time() - start_time, 2) 93 | exec_duration = f"The query '{query_string}' took **{duration} seconds** to execute." 94 | log.info(exec_duration) 95 | 96 | return completion.choices[0].message.content+"\n\n"+exec_duration -------------------------------------------------------------------------------- /chainlit.md: -------------------------------------------------------------------------------- 1 | # Welcome to the Catalyst Center API assistant 2 | 3 | Type in your Catalyst Center function you would like to do via REST API. Python code will follow! 4 | 5 | ### First time here? 6 | 7 | If you are here for the first time, type the following command to import the data: `importdata`. 8 | 9 | Enjoy! :) -------------------------------------------------------------------------------- /data/b_cisco_catalyst_center_user_guide_237.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flopach/create-your-own-api-assistant-cisco-catalyst-center/d2dba7f27dee3826c5ad1992d86068d9871499c2/data/b_cisco_catalyst_center_user_guide_237.pdf -------------------------------------------------------------------------------- /images/architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flopach/create-your-own-api-assistant-cisco-catalyst-center/d2dba7f27dee3826c5ad1992d86068d9871499c2/images/architecture.png -------------------------------------------------------------------------------- /images/example_clients_summary.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flopach/create-your-own-api-assistant-cisco-catalyst-center/d2dba7f27dee3826c5ad1992d86068d9871499c2/images/example_clients_summary.png -------------------------------------------------------------------------------- /images/example_eos.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flopach/create-your-own-api-assistant-cisco-catalyst-center/d2dba7f27dee3826c5ad1992d86068d9871499c2/images/example_eos.png -------------------------------------------------------------------------------- /images/example_spreadsheet.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flopach/create-your-own-api-assistant-cisco-catalyst-center/d2dba7f27dee3826c5ad1992d86068d9871499c2/images/example_spreadsheet.png -------------------------------------------------------------------------------- /images/inferencing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flopach/create-your-own-api-assistant-cisco-catalyst-center/d2dba7f27dee3826c5ad1992d86068d9871499c2/images/inferencing.png -------------------------------------------------------------------------------- /images/preparing-data.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flopach/create-your-own-api-assistant-cisco-catalyst-center/d2dba7f27dee3826c5ad1992d86068d9871499c2/images/preparing-data.png -------------------------------------------------------------------------------- /images/youtube-thumbnail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flopach/create-your-own-api-assistant-cisco-catalyst-center/d2dba7f27dee3826c5ad1992d86068d9871499c2/images/youtube-thumbnail.png -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | """ 2 | Cisco Sample Code License 1.1 3 | Author: flopach 2024 4 | """ 5 | from TalkToOpenAI import LLMOpenAI 6 | from TalkToOllama import LLMOllama 7 | from TalkToDatabase import VectorDB 8 | from ImportData import DataHandler 9 | import logging 10 | import chainlit as cl 11 | 12 | # ====================== 13 | # SETTINGS 14 | # ====================== 15 | 16 | # Select the LLM which you would like to use. 17 | # "openai" or "ollama". You can also define the specific model below 18 | setting_chosen_LLM = "openai" 19 | 20 | # Do you want to extend the API specification from scratch? 21 | # True = Your chosen LLM will generate the existing base API documentation. This can take several hours. 22 | # False = Use the already generated JSON file (generated with GPT-3.5-turbo) 23 | setting_full_import = False 24 | 25 | # ====================== 26 | # Instance creations 27 | # ====================== 28 | 29 | # set logging level 30 | log = logging.getLogger("applogger") 31 | logging.getLogger("applogger").setLevel(logging.DEBUG) 32 | 33 | if setting_chosen_LLM == "openai": 34 | # OpenAI: Create instance for Vector DB and LLM 35 | database = VectorDB("catcenter_vectors","openai","chromadb/") 36 | LLM = LLMOpenAI(database=database,model="gpt-3.5-turbo") 37 | else: 38 | # Open Source LLM: Create instance for Vector DB and LLM 39 | database = VectorDB("catcenter_vectors","ollama","chromadb/") 40 | LLM = LLMOllama(database=database,model="llama3") 41 | 42 | # Create DataHandler instance to import and embed data from local documents 43 | datahandler = DataHandler(database,LLM) 44 | 45 | # ====================== 46 | # Chainlit functions 47 | # docs: https://docs.chainlit.io/get-started/overview 48 | # ====================== 49 | 50 | @cl.on_chat_start 51 | def on_chat_start(): 52 | log.info("A new chat session has started!") 53 | 54 | 55 | @cl.on_message 56 | async def main(message: cl.Message): 57 | """ 58 | This function is called every time a user inputs a message in the UI. 59 | It sends back an intermediate response from the tool, followed by the final answer. 60 | 61 | Args: 62 | message: The user's message. 63 | """ 64 | 65 | # trick for loader: https://docs.chainlit.io/concepts/message 66 | msg = cl.Message(content="") 67 | await msg.send() 68 | 69 | # if the user only types "importdata", call the import_data() function 70 | if message.content == "importdata": 71 | msg.content = await import_data() 72 | else: 73 | # else, send the user_query to the LLM 74 | msg.content = await ask_llm(message.content) 75 | 76 | await msg.update() 77 | 78 | @cl.step 79 | async def ask_llm(query_string): 80 | """ 81 | Chainlit Step function: ask the LLM + return the result 82 | """ 83 | response = LLM.ask_llm(query_string) 84 | return response 85 | 86 | @cl.step 87 | async def import_data(): 88 | """ 89 | Chainlit Step function: Importing data to vectorDB 90 | """ 91 | # Import data from API documentation 92 | datahandler.scrape_apidocs_catcenter() 93 | 94 | # Import data from Catalyst Center PDF User Guide 95 | datahandler.scrape_pdfuserguide_catcenter("data/b_cisco_catalyst_center_user_guide_237.pdf") 96 | 97 | # Import API Specs Document 98 | if setting_full_import: 99 | datahandler.import_apispecs_generate_new_data("data/GA-2-3-7-swagger-v1.annotated.json") 100 | else: 101 | datahandler.import_apispecs_from_json() 102 | 103 | return "All data imported!" -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | package-mode = false 3 | name = "create-your-own-api-assistant-cisco-catalyst-center" 4 | version = "0.1.0" 5 | description = "" 6 | authors = ["flopach "] 7 | readme = "README.md" 8 | 9 | [tool.poetry.dependencies] 10 | python = "^3.11" 11 | openai = "^1.23.6" 12 | ollama = "^0.1.9" 13 | requests = "^2.31.0" 14 | chromadb = "^0.5.0" 15 | chainlit = "^1.0.505" 16 | pymupdf = "^1.24.2" 17 | beautifulsoup4 = "^4.12.3" 18 | 19 | [build-system] 20 | requires = ["poetry-core"] 21 | build-backend = "poetry.core.masonry.api" --------------------------------------------------------------------------------