├── requirements.txt ├── scripts └── startup.sh ├── docker-compose.yml ├── docker └── Dockerfile ├── README.md ├── netbox_react_agent ├── netbox_apis.json └── netbox_react_agent.py └── LICENSE /requirements.txt: -------------------------------------------------------------------------------- 1 | streamlit 2 | langchain_community 3 | openai 4 | -------------------------------------------------------------------------------- /scripts/startup.sh: -------------------------------------------------------------------------------- 1 | cd netbox_react_agent 2 | streamlit run netbox_react_agent.py -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.6' 2 | 3 | services: 4 | netbox_react_agent: 5 | image: johncapobianco/netbox_react_agent:netbox_react_agent 6 | container_name: netbox_react_agent 7 | restart: always 8 | build: 9 | context: ./ 10 | dockerfile: ./docker/Dockerfile 11 | ports: 12 | - "8501:8501" -------------------------------------------------------------------------------- /docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:latest 2 | 3 | ARG DEBIAN_FRONTEND=noninteractive 4 | 5 | RUN echo "==> Upgrading apk and installing system utilities ...." \ 6 | && apt -y update \ 7 | && apt-get install -y wget \ 8 | && apt-get -y install sudo 9 | 10 | RUN echo "==> Installing Python3 and pip ...." \ 11 | && apt-get install python3 -y \ 12 | && apt install python3-pip -y 13 | 14 | RUN echo "==> Install dos2unix..." \ 15 | && sudo apt-get install dos2unix -y 16 | 17 | RUN echo "==> Install requirements.." \ 18 | && pip install --break-system-packages -U --quiet langchain_community \ 19 | && pip install --break-system-packages streamlit --upgrade \ 20 | && pip install --break-system-packages openai 21 | 22 | COPY /netbox_react_agent /netbox_react_agent/ 23 | COPY /scripts /scripts/ 24 | 25 | RUN echo "==> Convert script..." \ 26 | && dos2unix /scripts/startup.sh 27 | 28 | CMD ["/bin/bash", "/scripts/startup.sh"] -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # netbox_react_agent 2 | An artificial intelligence ReAct Agent for NetBox 3 | 4 | Welcome to the NetBox AI Agent project! This application provides a natural language interface for interacting with NetBox APIs, enabling CRUD (Create, Read, Update, Delete) operations through an intuitive chat-like interface powered by AI. 5 | 6 | This project simplifies network management by combining AI-driven agents with the NetBox API to streamline and automate common network tasks. 7 | 8 | ## Branches Overview 9 | 10 | ### Main Branch 11 | 12 | Powered by ChatGPT (gpt-4o) 13 | 14 | Requires OpenAI API Key 15 | 16 | Offers high accuracy and performance for handling natural language queries. 17 | 18 | Recommended for production use. 19 | 20 | API costs apply. 21 | 22 | ### Ollama Branch 23 | 24 | Powered by Local LLM using Ollama 25 | 26 | Completely free and private: All computations happen locally. 27 | 28 | No external API calls required. 29 | 30 | Performance: Works well for basic tasks but is less sophisticated compared to the ChatGPT-based version. 31 | 32 | Recommended for personal or offline use cases. 33 | 34 | ## Features 35 | 36 | Natural Language Interface: Interact with NetBox APIs using plain English commands. 37 | 38 | CRUD Operations: Perform Create, Read, Update, and Delete tasks on your NetBox data. 39 | 40 | API Validation: Ensures commands align with supported NetBox API endpoints. 41 | 42 | Dynamic Tools: Auto-detects and leverages the appropriate tools for each task. 43 | 44 | Local or Cloud Options: Choose between the main branch for high performance or the Ollama branch for privacy and offline capabilities. 45 | 46 | ## Setup Instructions 47 | 48 | ### Prerequisites 49 | Docker and Docker Compose installed. 50 | 51 | OpenAI API Key (for the main branch). 52 | 53 | Optional: Ollama installed for the local branch. 54 | 55 | ## Quick Start 56 | 57 | ### Clone the Repository 58 | 59 | ``` bash 60 | git clone https://github.com//netbox-ai-agent.git 61 | cd netbox-ai-agent 62 | ``` 63 | 64 | ### Run the Application 65 | 66 | ```bash 67 | docker-compose up 68 | ``` 69 | 70 | ### Access the App 71 | 72 | Open your browser and go to http://localhost:8501. 73 | 74 | Configure API Keys 75 | 76 | ## For the main branch: 77 | 78 | Provide your NetBox API URL, NetBox Token, and OpenAI API Key in the configuration page. 79 | 80 | ## For the Ollama branch: 81 | Provide only your NetBox API URL and NetBox Token. 82 | 83 | # Start Chatting 84 | 85 | Use natural language to manage your NetBox data. Example commands: 86 | 87 | "Fetch all devices in the DC1 site." 88 | 89 | "Create a new VLAN in site DC2." 90 | 91 | ## Key Components 92 | 93 | NetBoxController: Manages interactions with the NetBox API. 94 | 95 | LangChain ReAct Agent: Dynamically selects tools to process natural language queries. 96 | 97 | Streamlit Interface: Provides an intuitive chat-like web UI. 98 | 99 | ## FAQs 100 | 101 | Q: Which branch should I use? 102 | 103 | Use the main branch for production-grade performance and OpenAI's latest capabilities. 104 | 105 | Use the Ollama branch for offline and private operations, but expect reduced performance. 106 | 107 | Q: How do I switch between branches? 108 | 109 | To use the Ollama branch, run: 110 | 111 | ```bash 112 | git checkout ollama 113 | ``` 114 | 115 | Then re-run the Docker setup. 116 | 117 | ## Troubleshooting 118 | 119 | Docker Issues: Ensure Docker is running and your system meets the necessary prerequisites. 120 | 121 | OpenAI Key Errors: Check that your API key is valid and added correctly. 122 | 123 | NetBox API Errors: Verify your NetBox instance is accessible, and the API token has the required permissions. 124 | -------------------------------------------------------------------------------- /netbox_react_agent/netbox_apis.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "URL": "/api/ipam/aggregates/", 4 | "Name": "Aggregates" 5 | }, 6 | { 7 | "URL": "/api/ipam/asns/", 8 | "Name": "ASNs" 9 | }, 10 | { 11 | "URL": "/api/dcim/cables/", 12 | "Name": "Cables" 13 | }, 14 | { 15 | "URL": "/api/circuits/circuit-terminations/", 16 | "Name": "Circuit Terminations" 17 | }, 18 | { 19 | "URL": "/api/circuits/circuit-types/", 20 | "Name": "Circuit Types" 21 | }, 22 | { 23 | "URL": "/api/circuits/circuits/", 24 | "Name": "Circuits" 25 | }, 26 | { 27 | "URL": "/api/virtualization/cluster-groups/", 28 | "Name": "Cluster Groups" 29 | }, 30 | { 31 | "URL": "/api/virtualization/cluster-types/", 32 | "Name": "Cluster Types" 33 | }, 34 | { 35 | "URL": "/api/virtualization/clusters/", 36 | "Name": "Clusters" 37 | }, 38 | { 39 | "URL": "/api/dcim/device-types/", 40 | "Name": "Device Types" 41 | }, 42 | { 43 | "URL": "/api/dcim/devices/", 44 | "Name": "Devices" 45 | }, 46 | { 47 | "URL": "/api/ipam/ip-addresses/", 48 | "Name": "IP Addresses" 49 | }, 50 | { 51 | "URL": "/api/dcim/sites/", 52 | "Name": "Sites" 53 | }, 54 | { 55 | "URL": "/api/dcim/racks/", 56 | "Name": "Racks" 57 | }, 58 | { 59 | "URL": "/api/dcim/console-port-templates/", 60 | "Name": "Console Port Templates" 61 | }, 62 | { 63 | "URL": "/api/dcim/console-ports/", 64 | "Name": "Console Ports" 65 | }, 66 | { 67 | "URL": "/api/tenancy/contact-assignments/", 68 | "Name": "Contact Assignments" 69 | }, 70 | { 71 | "URL": "/api/tenancy/contact-groups/", 72 | "Name": "Contact Groups" 73 | }, 74 | { 75 | "URL": "/api/tenancy/contact-roles/", 76 | "Name": "Contact Roles" 77 | }, 78 | { 79 | "URL": "/api/tenancy/contacts/", 80 | "Name": "Contacts" 81 | }, 82 | { 83 | "URL": "/api/dcim/device-bay-templates/", 84 | "Name": "Device Bay Templates" 85 | }, 86 | { 87 | "URL": "/api/dcim/device-bays/", 88 | "Name": "Device Bays" 89 | }, 90 | { 91 | "URL": "/api/dcim/device-roles/", 92 | "Name": "Device Roles" 93 | }, 94 | { 95 | "URL": "/api/dcim/front-port-templates/", 96 | "Name": "Front Port Templates" 97 | }, 98 | { 99 | "URL": "/api/dcim/front-ports/", 100 | "Name": "Front Ports" 101 | }, 102 | { 103 | "URL": "/api/users/groups/", 104 | "Name": "Groups" 105 | }, 106 | { 107 | "URL": "/api/dcim/interface-templates/", 108 | "Name": "Interface Templates" 109 | }, 110 | { 111 | "URL": "/api/dcim/interfaces/", 112 | "Name": "Interfaces" 113 | }, 114 | { 115 | "URL": "/api/dcim/inventory-items/", 116 | "Name": "Inventory Items" 117 | }, 118 | { 119 | "URL": "/api/ipam/ip-ranges/", 120 | "Name": "Ip Ranges" 121 | }, 122 | { 123 | "URL": "/api/dcim/locations/", 124 | "Name": "Locations" 125 | }, 126 | { 127 | "URL": "/api/dcim/manufacturers/", 128 | "Name": "Manufacturers" 129 | }, 130 | { 131 | "URL": "/api/dcim/module-bay-templates/", 132 | "Name": "Module Bay Templates" 133 | }, 134 | { 135 | "URL": "/api/dcim/module-bays/", 136 | "Name": "Module Bays" 137 | }, 138 | { 139 | "URL": "/api/dcim/module-types/", 140 | "Name": "Module Types" 141 | }, 142 | { 143 | "URL": "/api/dcim/modules/", 144 | "Name": "Modules" 145 | }, 146 | { 147 | "URL": "/api/dcim/platforms/", 148 | "Name": "Platforms" 149 | }, 150 | { 151 | "URL": "/api/dcim/power-feeds/", 152 | "Name": "Power Feeds" 153 | }, 154 | { 155 | "URL": "/api/dcim/power-outlet-templates/", 156 | "Name": "Power Outlet Templates" 157 | }, 158 | { 159 | "URL": "/api/dcim/power-outlets/", 160 | "Name": "Power Outlets" 161 | }, 162 | { 163 | "URL": "/api/dcim/power-panels/", 164 | "Name": "Power Panels" 165 | }, 166 | { 167 | "URL": "/api/dcim/power-port-templates/", 168 | "Name": "Power Port Templates" 169 | }, 170 | { 171 | "URL": "/api/dcim/power-ports/", 172 | "Name": "Power Ports" 173 | }, 174 | { 175 | "URL": "/api/ipam/prefixes/", 176 | "Name": "Prefixes" 177 | }, 178 | { 179 | "URL": "/api/circuits/provider-networks/", 180 | "Name": "Provider Networks" 181 | }, 182 | { 183 | "URL": "/api/circuits/providers/", 184 | "Name": "Providers" 185 | }, 186 | { 187 | "URL": "/api/dcim/rack-reservations/", 188 | "Name": "Rack Reservations" 189 | }, 190 | { 191 | "URL": "/api/dcim/rack-roles/", 192 | "Name": "Rack Roles" 193 | }, 194 | { 195 | "URL": "/api/dcim/rear-port-templates/", 196 | "Name": "Rear Port Templates" 197 | }, 198 | { 199 | "URL": "/api/dcim/rear-ports/", 200 | "Name": "Rear Ports" 201 | }, 202 | { 203 | "URL": "/api/dcim/regions/", 204 | "Name": "Regions" 205 | }, 206 | { 207 | "URL": "/api/ipam/rirs/", 208 | "Name": "Rirs" 209 | }, 210 | { 211 | "URL": "/api/ipam/roles/", 212 | "Name": "Roles" 213 | }, 214 | { 215 | "URL": "/api/ipam/route-targets/", 216 | "Name": "Route Targets" 217 | }, 218 | { 219 | "URL": "/api/ipam/service-templates/", 220 | "Name": "Service Templates" 221 | }, 222 | { 223 | "URL": "/api/ipam/services/", 224 | "Name": "Services" 225 | }, 226 | { 227 | "URL": "/api/dcim/site-groups/", 228 | "Name": "Site Groups" 229 | }, 230 | { 231 | "URL": "/api/status/", 232 | "Name": "Status" 233 | }, 234 | { 235 | "URL": "/api/tenancy/tenant-groups/", 236 | "Name": "Tenant Groups" 237 | }, 238 | { 239 | "URL": "/api/tenancy/tenants/", 240 | "Name": "Tenants" 241 | }, 242 | { 243 | "URL": "/api/users/tokens/", 244 | "Name": "Tokens" 245 | }, 246 | { 247 | "URL": "/api/users/users/", 248 | "Name": "Users" 249 | }, 250 | { 251 | "URL": "/api/dcim/virtual-chassis/", 252 | "Name": "Virtual Chassis" 253 | }, 254 | { 255 | "URL": "/api/virtualization/interfaces/", 256 | "Name": "Interfaces" 257 | }, 258 | { 259 | "URL": "/api/virtualization/virtual-machines/", 260 | "Name": "Virtual Machines" 261 | }, 262 | { 263 | "URL": "/api/ipam/vlan-groups/", 264 | "Name": "Vlan Groups" 265 | }, 266 | { 267 | "URL": "/api/ipam/vlans/", 268 | "Name": "Vlans" 269 | }, 270 | { 271 | "URL": "/api/ipam/vrfs/", 272 | "Name": "Vrfs" 273 | } 274 | ] 275 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /netbox_react_agent/netbox_react_agent.py: -------------------------------------------------------------------------------- 1 | import os 2 | import json 3 | import logging 4 | import requests 5 | import difflib 6 | import streamlit as st 7 | from langchain_community.chat_models import ChatOpenAI 8 | from langchain.agents import AgentExecutor, create_react_agent 9 | from langchain.prompts import PromptTemplate 10 | from langchain_core.tools import tool, render_text_description 11 | import urllib3 12 | 13 | # Configure logging 14 | logging.basicConfig(level=logging.INFO) 15 | urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) 16 | 17 | # Global variables for lazy initialization 18 | llm = None 19 | agent_executor = None 20 | 21 | # NetBoxController for CRUD Operations 22 | class NetBoxController: 23 | def __init__(self, netbox_url, api_token): 24 | self.netbox = netbox_url.rstrip('/') 25 | self.api_token = api_token 26 | self.headers = { 27 | 'Accept': 'application/json', 28 | 'Authorization': f"Token {self.api_token}", 29 | } 30 | 31 | def get_api(self, api_url: str, params: dict = None): 32 | response = requests.get( 33 | f"{self.netbox}{api_url}", 34 | headers=self.headers, 35 | params=params, 36 | verify=False 37 | ) 38 | response.raise_for_status() 39 | return response.json() 40 | 41 | def post_api(self, api_url: str, payload: dict): 42 | response = requests.post( 43 | f"{self.netbox}{api_url}", 44 | headers=self.headers, 45 | json=payload, 46 | verify=False 47 | ) 48 | response.raise_for_status() 49 | return response.json() 50 | 51 | def delete_api(self, api_url: str): 52 | response = requests.delete( 53 | f"{self.netbox}{api_url}", 54 | headers=self.headers, 55 | verify=False 56 | ) 57 | response.raise_for_status() 58 | return response.json() 59 | 60 | 61 | # Function to load supported URLs with their names from a JSON file 62 | def load_urls(file_path='netbox_apis.json'): 63 | if not os.path.exists(file_path): 64 | return {"error": f"URLs file '{file_path}' not found."} 65 | try: 66 | with open(file_path, 'r') as f: 67 | data = json.load(f) 68 | return [(entry['URL'], entry.get('Name', '')) for entry in data] 69 | except Exception as e: 70 | return {"error": f"Error loading URLs: {str(e)}"} 71 | 72 | 73 | def check_url_support(api_url: str) -> dict: 74 | url_list = load_urls() 75 | if "error" in url_list: 76 | return url_list # Return error if loading URLs failed 77 | 78 | urls = [entry[0] for entry in url_list] 79 | names = [entry[1] for entry in url_list] 80 | 81 | close_url_matches = difflib.get_close_matches(api_url, urls, n=1, cutoff=0.6) 82 | close_name_matches = difflib.get_close_matches(api_url, names, n=1, cutoff=0.6) 83 | 84 | if close_url_matches: 85 | closest_url = close_url_matches[0] 86 | matching_name = [entry[1] for entry in url_list if entry[0] == closest_url][0] 87 | return {"status": "supported", "closest_url": closest_url, "closest_name": matching_name} 88 | elif close_name_matches: 89 | closest_name = close_name_matches[0] 90 | closest_url = [entry[0] for entry in url_list if entry[1] == closest_name][0] 91 | return {"status": "supported", "closest_url": closest_url, "closest_name": closest_name} 92 | else: 93 | return {"status": "unsupported", "message": f"The input '{api_url}' is not supported."} 94 | 95 | 96 | # Tools for interacting with NetBox 97 | @tool 98 | def discover_apis(dummy_input: str = None) -> dict: 99 | """Discover available NetBox APIs from a local JSON file.""" 100 | try: 101 | if not os.path.exists("netbox_apis.json"): 102 | return {"error": "API JSON file not found. Please ensure 'netbox_apis.json' exists in the project directory."} 103 | 104 | with open("netbox_apis.json", "r") as f: 105 | data = json.load(f) 106 | return {"apis": data, "message": "APIs successfully loaded from JSON file"} 107 | except Exception as e: 108 | return {"error": f"An error occurred while loading the APIs: {str(e)}"} 109 | 110 | @tool 111 | def check_supported_url_tool(api_url: str) -> dict: 112 | """Check if an API URL or Name is supported by NetBox.""" 113 | result = check_url_support(api_url) 114 | if result.get('status') == 'supported': 115 | closest_url = result['closest_url'] 116 | closest_name = result['closest_name'] 117 | return { 118 | "status": "supported", 119 | "message": f"The closest supported API URL is '{closest_url}' ({closest_name}).", 120 | "action": { 121 | "next_tool": "get_netbox_data_tool", 122 | "input": closest_url 123 | } 124 | } 125 | return result 126 | 127 | @tool 128 | def get_netbox_data_tool(api_url: str) -> dict: 129 | """Fetch data from NetBox.""" 130 | try: 131 | netbox_controller = NetBoxController( 132 | netbox_url=os.getenv("NETBOX_URL"), 133 | api_token=os.getenv("NETBOX_TOKEN") 134 | ) 135 | data = netbox_controller.get_api(api_url) 136 | return data 137 | except requests.HTTPError as e: 138 | return {"error": f"Failed to fetch data from NetBox: {str(e)}"} 139 | except Exception as e: 140 | return {"error": f"An unexpected error occurred: {str(e)}"} 141 | 142 | @tool 143 | def create_netbox_data_tool(input: str) -> dict: 144 | """Create new data in NetBox.""" 145 | try: 146 | data = json.loads(input) 147 | api_url = data.get("api_url") 148 | payload = data.get("payload") 149 | 150 | if not api_url or not payload: 151 | raise ValueError("Both 'api_url' and 'payload' must be provided.") 152 | 153 | if not isinstance(payload, dict): 154 | raise ValueError("Payload must be a dictionary.") 155 | 156 | netbox_controller = NetBoxController( 157 | netbox_url=os.getenv("NETBOX_URL"), 158 | api_token=os.getenv("NETBOX_TOKEN") 159 | ) 160 | return netbox_controller.post_api(api_url, payload) 161 | except Exception as e: 162 | return {"error": f"An error occurred in create_netbox_data_tool: {str(e)}"} 163 | 164 | @tool 165 | def delete_netbox_data_tool(api_url: str) -> dict: 166 | """Delete data from NetBox.""" 167 | try: 168 | netbox_controller = NetBoxController( 169 | netbox_url=os.getenv("NETBOX_URL"), 170 | api_token=os.getenv("NETBOX_TOKEN") 171 | ) 172 | return netbox_controller.delete_api(api_url) 173 | except requests.HTTPError as e: 174 | return {"error": f"Failed to delete data from NetBox: {str(e)}"} 175 | except Exception as e: 176 | return {"error": f"An unexpected error occurred: {str(e)}"} 177 | 178 | def process_agent_response(response): 179 | if response and response.get("status") == "supported" and "next_tool" in response.get("action", {}): 180 | next_tool = response["action"]["next_tool"] 181 | tool_input = response["action"]["input"] 182 | 183 | # Automatically invoke the next tool 184 | return agent_executor.invoke({ 185 | "input": tool_input, 186 | "chat_history": st.session_state.chat_history, 187 | "agent_scratchpad": "", 188 | "tool": next_tool 189 | }) 190 | else: 191 | return response 192 | 193 | # ============================================================ 194 | # Streamlit App 195 | # ============================================================ 196 | 197 | def configure_page(): 198 | st.title("NetBox Configuration") 199 | base_url = st.text_input("NetBox URL", placeholder="https://demo.netbox.dev") 200 | api_token = st.text_input("NetBox API Token", type="password", placeholder="Your API Token") 201 | openai_key = st.text_input("OpenAI API Key", type="password", placeholder="Your OpenAI API Key") 202 | 203 | if st.button("Save and Continue"): 204 | if not base_url or not api_token or not openai_key: 205 | st.error("All fields are required.") 206 | else: 207 | st.session_state['NETBOX_URL'] = base_url 208 | st.session_state['NETBOX_TOKEN'] = api_token 209 | st.session_state['OPENAI_API_KEY'] = openai_key 210 | os.environ['NETBOX_URL'] = base_url 211 | os.environ['NETBOX_TOKEN'] = api_token 212 | os.environ['OPENAI_API_KEY'] = openai_key 213 | st.success("Configuration saved! Redirecting to chat...") 214 | st.session_state['page'] = "chat" 215 | 216 | def initialize_agent(): 217 | global llm, agent_executor 218 | if not llm: 219 | # Initialize the LLM with the API key from session state 220 | llm = ChatOpenAI(model_name="gpt-4o", openai_api_key=st.session_state['OPENAI_API_KEY']) 221 | 222 | # Define tools 223 | tools = [discover_apis, check_supported_url_tool, get_netbox_data_tool, create_netbox_data_tool, delete_netbox_data_tool] 224 | 225 | # Create the prompt template 226 | tool_descriptions = render_text_description(tools) 227 | # Create the PromptTemplate 228 | template = """ 229 | Assistant is a network assistant capable of managing NetBox data using CRUD operations. 230 | 231 | TOOLS: 232 | - discover_apis: Discovers available NetBox APIs from a local JSON file. 233 | - check_supported_url_tool: Checks if an API URL or Name is supported by NetBox. 234 | - get_netbox_data_tool: Fetches data from NetBox using the specified API URL. 235 | - create_netbox_data_tool: Creates new data in NetBox using the specified API URL and payload. 236 | - delete_netbox_data_tool: Deletes data from NetBox using the specified API URL. 237 | 238 | GUIDELINES: 239 | 1. Use 'check_supported_url_tool' to validate ambiguous or unknown URLs or Names. 240 | 2. If certain about the URL, directly use 'get_netbox_data_tool', 'create_netbox_data_tool', or 'delete_netbox_data_tool'. 241 | 3. Follow a structured response format to ensure consistency. 242 | 243 | FORMAT: 244 | Thought: [Your thought process] 245 | Action: [Tool Name] 246 | Action Input: [Tool Input] 247 | Observation: [Tool Response] 248 | Final Answer: [Your response to the user] 249 | 250 | Begin: 251 | 252 | Previous conversation history: 253 | {chat_history} 254 | 255 | New input: {input} 256 | 257 | {agent_scratchpad} 258 | """ 259 | prompt_template = PromptTemplate( 260 | template=template, 261 | input_variables=["input", "chat_history", "agent_scratchpad"], 262 | partial_variables={ 263 | "tools": tool_descriptions, 264 | "tool_names": ", ".join([t.name for t in tools]) 265 | } 266 | ) 267 | 268 | # Create the ReAct agent 269 | agent = create_react_agent(llm=llm, tools=tools, prompt=prompt_template) 270 | 271 | # Create the AgentExecutor 272 | agent_executor = AgentExecutor( 273 | agent=agent, 274 | tools=tools, 275 | handle_parsing_errors=True, 276 | verbose=True, 277 | max_iterations=10 278 | ) 279 | 280 | def chat_page(): 281 | st.title("Chat with NetBox AI Agent") 282 | user_input = st.text_input("Ask NetBox a question:", key="user_input") 283 | 284 | # Ensure the agent is initialized 285 | if "OPENAI_API_KEY" not in st.session_state: 286 | st.error("Please configure NetBox and OpenAI settings first!") 287 | st.session_state['page'] = "configure" 288 | return 289 | 290 | initialize_agent() 291 | 292 | # Initialize session state variables if not already set 293 | if "chat_history" not in st.session_state: 294 | st.session_state.chat_history = "" 295 | 296 | if "conversation" not in st.session_state: 297 | st.session_state.conversation = [] 298 | 299 | # Button to submit the question 300 | if st.button("Send"): 301 | if user_input: 302 | # Add the user input to the conversation history 303 | st.session_state.conversation.append({"role": "user", "content": user_input}) 304 | 305 | # Invoke the agent with the user input and current chat history 306 | try: 307 | response = agent_executor.invoke({ 308 | "input": user_input, 309 | "chat_history": st.session_state.chat_history, 310 | "agent_scratchpad": "" # Initialize agent scratchpad as an empty string 311 | }) 312 | 313 | # Process the agent's response 314 | final_response = process_agent_response(response) 315 | 316 | # Extract the final answer 317 | final_answer = final_response.get('output', 'No answer provided.') 318 | 319 | # Display the question and answer 320 | st.write(f"**Question:** {user_input}") 321 | st.write(f"**Answer:** {final_answer}") 322 | 323 | # Add the response to the conversation history 324 | st.session_state.conversation.append({"role": "assistant", "content": final_answer}) 325 | 326 | # Update chat history with the new conversation 327 | st.session_state.chat_history = "\n".join( 328 | [f"{entry['role'].capitalize()}: {entry['content']}" for entry in st.session_state.conversation] 329 | ) 330 | except Exception as e: 331 | st.error(f"An error occurred: {str(e)}") 332 | 333 | # Display conversation history 334 | if st.session_state.conversation: 335 | st.markdown("### Conversation History") 336 | for entry in st.session_state.conversation: 337 | if entry["role"] == "user": 338 | st.markdown(f"**User:** {entry['content']}") 339 | elif entry["role"] == "assistant": 340 | st.markdown(f"**NetBox AI ReAct Agent:** {entry['content']}") 341 | 342 | # Page Navigation 343 | if 'page' not in st.session_state: 344 | st.session_state['page'] = "configure" 345 | 346 | if st.session_state['page'] == "configure": 347 | configure_page() 348 | elif st.session_state['page'] == "chat": 349 | chat_page() --------------------------------------------------------------------------------