├── .gitignore ├── Dockerfile ├── LICENSE ├── README.md ├── config ├── __init__.py └── config.py ├── docs └── sheets_help.md ├── example.env ├── main.py ├── poetry.lock ├── pyproject.toml ├── requirements.txt ├── tools ├── __init__.py ├── bs_tool.py ├── create_draft_tool.py ├── file_append_tool.py ├── file_count_lines.py ├── file_create_tool.py ├── file_edit_tool.py ├── file_line_read_tool.py ├── file_tool_(depricated).py ├── folder_tool.py └── interpreter_tool.py └── utils ├── __init__.py ├── agent_crew_llm.py ├── callable_registry.py ├── cli_parser.py ├── groq.py ├── helpers.py ├── import_package_modules.py ├── ollama_loader.py ├── safe_argment_parser.py ├── sheets_loader.py ├── tools_llm_config.py └── tools_mapping.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | #Mac 10 | .DS_Store 11 | 12 | # Distribution / packaging 13 | .Python 14 | build/ 15 | develop-eggs/ 16 | dist/ 17 | downloads/ 18 | eggs/ 19 | .eggs/ 20 | lib/ 21 | lib64/ 22 | parts/ 23 | sdist/ 24 | var/ 25 | wheels/ 26 | share/python-wheels/ 27 | *.egg-info/ 28 | .installed.cfg 29 | *.egg 30 | MANIFEST 31 | 32 | # PyInstaller 33 | # Usually these files are written by a python script from a template 34 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 35 | *.manifest 36 | *.spec 37 | 38 | # Installer logs 39 | pip-log.txt 40 | pip-delete-this-directory.txt 41 | 42 | # Unit test / coverage reports 43 | htmlcov/ 44 | .tox/ 45 | .nox/ 46 | .coverage 47 | .coverage.* 48 | .cache 49 | nosetests.xml 50 | coverage.xml 51 | *.cover 52 | *.py,cover 53 | .hypothesis/ 54 | .pytest_cache/ 55 | cover/ 56 | 57 | # Translations 58 | *.mo 59 | *.pot 60 | 61 | # Django stuff: 62 | *.log 63 | local_settings.py 64 | db.sqlite3 65 | db.sqlite3-journal 66 | 67 | # Flask stuff: 68 | instance/ 69 | .webassets-cache 70 | 71 | # Scrapy stuff: 72 | .scrapy 73 | 74 | # Sphinx documentation 75 | docs/_build/ 76 | 77 | # PyBuilder 78 | .pybuilder/ 79 | target/ 80 | 81 | # Jupyter Notebook 82 | .ipynb_checkpoints 83 | 84 | # IPython 85 | profile_default/ 86 | ipython_config.py 87 | 88 | # pyenv 89 | # For a library or package, you might want to ignore these files since the code is 90 | # intended to run in multiple environments; otherwise, check them in: 91 | # .python-version 92 | 93 | # pipenv 94 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 95 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 96 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 97 | # install all needed dependencies. 98 | #Pipfile.lock 99 | 100 | # poetry 101 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 102 | # This is especially recommended for binary packages to ensure reproducibility, and is more 103 | # commonly ignored for libraries. 104 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 105 | #poetry.lock 106 | 107 | # pdm 108 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 109 | #pdm.lock 110 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 111 | # in version control. 112 | # https://pdm.fming.dev/#use-with-ide 113 | .pdm.toml 114 | 115 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 116 | __pypackages__/ 117 | 118 | # Celery stuff 119 | celerybeat-schedule 120 | celerybeat.pid 121 | 122 | # SageMath parsed files 123 | *.sage.py 124 | 125 | # Environments 126 | .env 127 | .venv 128 | env/ 129 | venv/ 130 | ENV/ 131 | env.bak/ 132 | venv.bak/ 133 | 134 | # Spyder project settings 135 | .spyderproject 136 | .spyproject 137 | 138 | # Rope project settings 139 | .ropeproject 140 | 141 | # mkdocs documentation 142 | /site 143 | 144 | # mypy 145 | .mypy_cache/ 146 | .dmypy.json 147 | dmypy.json 148 | 149 | # Pyre type checker 150 | .pyre/ 151 | 152 | # pytype static type analyzer 153 | .pytype/ 154 | 155 | # Cython debug symbols 156 | cython_debug/ 157 | 158 | # PyCharm 159 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 160 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 161 | # and can be added to the global gitignore or merged into this file. For a more nuclear 162 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 163 | #.idea/ 164 | .DS_Store 165 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Use a base image with Python 3.11.8 2 | FROM python:3.11.8 3 | 4 | # Install updated pip, setuptools, and wheel 5 | RUN pip install --upgrade pip setuptools wheel 6 | 7 | # Install crewai and its tools 8 | RUN pip install poetry 9 | 10 | # Clone crewai-sheet-ui 11 | RUN git clone https://github.com/yuriwa/crewai-sheets-ui.git /home/user/root/crewai-sheets-ui 12 | # Set the working directory in the Docker image 13 | WORKDIR /home/user/root/crewai-sheets-ui 14 | 15 | ENV PEP517_BUILD_BACKEND=setuptools.build_meta 16 | 17 | # Configure poetry to not create a virtual environment and install dependencies 18 | RUN poetry config virtualenvs.create false && poetry install 19 | 20 | RUN pip install langchain_groq 21 | RUN pip install sentry-sdk 22 | 23 | RUN mkdir /home/user/root/ENV 24 | # Create an .env file and add the exports 25 | RUN echo "export VAR1=value1\nexport VAR2=value2\nexport VAR3=value3\nexport VAR4=value4\nexport VAR5=value5\nexport VAR6=value6\nexport VAR7=value7\nexport VAR8=value8\nexport VAR9=value9" > /home/user/root/crewai-sheets-ui/../ENV/.env 26 | 27 | # Expose port 11434 28 | #EXPOSE 11434 29 | 30 | WORKDIR /home/user/root/savefiles 31 | 32 | CMD if [ -z "$OPENAI_API_KEY" ]; then \ 33 | echo "See https://github.com/yuriwa/crewai-sheets-ui for instructions on how run this Docker container" && \ 34 | echo "Minimal usage: docker run -it -p 11434:11434 -v $(pwd)/savefiles:/home/user/root/savefiles -e OPENAI_API_KEY='YOUR API KEY' crewai-image" && \ 35 | echo "You can replace $(pwd)$(pwd)/savefiles with the path to your savefiles folder"; \ 36 | elif [ ! -d "/home/user/root/savefiles" ]; then \ 37 | echo "The required volume is not mounted." && \ 38 | echo "See https://github.com/yuriwa/crewai-sheets-ui for instructions on how run this Docker container" && \ 39 | echo "Minimal usage: docker run -it -p 11434:11434 -v $(pwd)/savefiles:/home/user/root/savefiles -e OPENAI_API_KEY='YOUR API KEY' crewai-image" && \ 40 | echo "You can replace $(pwd)$(pwd)/savefiles with the path to your savefiles folder"; \ 41 | fi 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 yuriwa 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![# crewai-sheets-ui Project](https://repository-images.githubusercontent.com/778369177/0b532ef9-0315-49f6-9edf-83496ae0f399) 2 | 3 | 4 | 5 | # Motivation 6 | 7 | Inspired by the capabilities of CrewAI, I realized the power of automation could be more accessible. This project is about sharing that power—helping friends and colleagues harness AI to streamline their tasks, even if they aren't deep into coding themselves. It’s about making sophisticated technology approachable for anyone interested in automating the routine, allowing them to focus on their passions. 8 | 9 | # Features 10 | 11 | ## Staff 12 | - **GPT Agents**: Offers a set of extendable GPT agents. Choose from predefined options or add your custom agents to fit your specific needs. 13 | 14 | ## Projects 15 | - **Project Management**: Keep all your crew assignments in one convenient location. Choose and manage projects with a single click, making it simpler to focus on what really matters. 16 | 17 | ## Tools 18 | - **Extensive Tools Library**: From `crewai-sheets-ui` to `crewai-tools`, including the latest from `langchain` and `langchain-community`. 19 | - **Tool Integration Made Easy**: Add tools from the `langchain` collections directly—just like that. 20 | - **Custom Tool Addition**: Easily configure and integrate your own tools. 21 | - **Executor Tool**: A powerful feature based on `open-interpreter` that runs commands and crafts code for tools not yet supported. 22 | 23 | ## Model Management 24 | - **Rapid Model Switching**: Switch between LLM models for various functions effortlessly—whether it’s for different agents, tasks, or entire toolsets. 25 | - **Detailed LLM Configurations**: Set precise configurations for each model and tool, offering you full control over their application. 26 | - **Comprehensive Model Support**: Compatible with major LLM providers such as OpenAI, Azure, Anthropic, Groq, and Hugging Face. Integrate any model from these providers with a simple click. 27 | 28 | ## Local and Online Model Support 29 | - **Local Models**: Fully supports local models, giving you the flexibility to run operations offline or use specific models that aren’t available online. 30 | - **Groq Rate Throttling**: Efficiently utilize Groq’s API without worrying about hitting usage caps. 31 | 32 | ## User Experience 33 | - **Easy Startup with Docker**: Get started quickly and safely using Docker, ensuring a secure and clean setup. 34 | - **Familiar Interface**: Leveraging a Google Sheets UI, this tool brings advanced automation into an easy-to-use, familiar format, perfect for anyone looking to dive into automation without the steep learning curve. 35 | 36 | 37 | # Setup Guide for Running with Docker (for users) 38 | 39 | This guide provides instructions for setting up and running a Docker container for your application, using various external APIs for enhanced functionality. 40 | 41 | ## Prerequisites: 42 | - **Check if Docker is installed:** 43 | - **Windows/Linux/MacOS:** Run `docker --version` in your command prompt or terminal. If Docker is installed, you will see the version number. If not, follow the installation link below. 44 | - **Install Docker (if not installed):** 45 | - [Docker Installation Guide](https://docs.docker.com/get-docker/) 46 | 47 | ### API Keys: 48 | You will need to obtain API keys from the following providers. A single API key is sufficient. You don't need all: 49 | Optionally, if you want to run your LLM locally, without a cloud provider, install [Ollama](https://ollama.com/) 50 | 51 | - **OpenAI**: [OpenAI API Keys](http://platform.openai.com/) 52 | - **Anthropic API**: [Anthropic API Access](https://www.anthropic.com/api) 53 | - **Groq API**: [Groq API Details](https://console.groq.com/playground) This is FREE at the moment. 54 | - **Hugging Face Hub**: [Hugging Face API Tokens](https://huggingface.co/settings/tokens) Some models FREE at the moment. 55 | - **Azure OpenAI**: [Azure OpenAI Documentation](https://docs.microsoft.com/en-us/azure/cognitive-services/openai/) (mainly for Enterprises) 56 | 57 | Optionally, Serper API if you want to use Serper instead of DuckDuckGo. 58 | - **Serper API**: [Serper API Documentation](https://serpapi.com/) 59 | 60 | ## Running the Container: 61 | - Replace any API KEYS that you have in the below. Do not edit anything else. 62 | - Copy the command for your system to your terminal or powershell. 63 | 64 | - **Linux/MacOS:** 65 | ```bash 66 | mkdir -p ./savefiles && \ 67 | docker build -t crewai-image https://github.com/yuriwa/crewai-sheets-ui.git && \ 68 | docker run -it -p 11434:11434 \ 69 | -v $(pwd)/savefiles:/home/user/root/savefiles \ 70 | -e AZURE_OPENAI_KEY='CHANGE THIS TO YOUR AZURE_OPENAI_KEY' \ 71 | -e SECRET_OPENAI_API_KEY='CHANGE THIS TO YOUR SECRET_OPENAI_API_KEY' \ 72 | -e SERPER_API_KEY='CHANGE THIS TO YOUR SERPER_API_KEY' \ 73 | -e AZURE_OPENAI_VERSION='2024-02-15-preview' \ 74 | -e AZURE_OPENAI_API_KEY='CHANGE THIS TO YOUR AZURE_OPENAI_API_KEY' \ 75 | -e AZURE_OPENAI_ENDPOINT='CHANGE THIS TO YOUR AZURE_OPENAI_ENDPOINT' \ 76 | -e ANTHROPIC_API_KEY='CHANGE THIS TO YOUR ANTHROPIC_API_KEY' \ 77 | -e GROQ_API_KEY='CHANGE THIS TO YOUR GROQ_API_KEY' \ 78 | -e HUGGINGFACEHUB_API_TOKEN='CHANGE THIS TO YOUR HUGGINGFACEHUB_API_TOKEN' \ 79 | -e OPENAI_API_KEY='DONT CHANGE THIS USE SECRET OPENAIAPIKEY' \ 80 | crewai-image python /home/user/root/crewai-sheets-ui/main.py 81 | 82 | ``` 83 | 84 | - **Windows (PowerShell):** 85 | ``` 86 | New-Item -ItemType Directory -Path .\savefiles -Force; ` 87 | docker build -t crewai-image https://github.com/yuriwa/crewai-sheets-ui.git; ` 88 | docker run -it -p 11434:11434 ` 89 | -v ${PWD}\savefiles:/home/user/root/savefiles ` 90 | -e AZURE_OPENAI_KEY='CHANGE THIS TO YOUR AZURE_OPENAI_KEY' ` 91 | -e SECRET_OPENAI_API_KEY='CHANGE THIS TO YOUR SECRET_OPENAI_API_KEY' ` 92 | -e SERPER_API_KEY='CHANGE THIS TO YOUR SERPER_API_KEY' ` 93 | -e AZURE_OPENAI_VERSION='2024-02-15-preview' ` 94 | -e AZURE_OPENAI_API_KEY='CHANGE THIS TO YOUR AZURE_OPENAI_API_KEY' ` 95 | -e AZURE_OPENAI_ENDPOINT='CHANGE THIS TO YOUR AZURE_OPENAI_ENDPOINT' ` 96 | -e ANTHROPIC_API_KEY='CHANGE THIS TO YOUR ANTHROPIC_API_KEY' ` 97 | -e GROQ_API_KEY='CHANGE THIS TO YOUR GROQ_API_KEY' ` 98 | -e HUGGINGFACEHUB_API_TOKEN='CHANGE THIS TO YOUR HUGGINGFACEHUB_API_TOKEN' ` 99 | -e OPENAI_API_KEY='DONT CHANGE THIS USE SECRET OPENAIAPIKEY' ` 100 | crewai-image python /home/user/root/crewai-sheets-ui/main.py 101 | ``` 102 | 103 | ### Notes: 104 | - Ensure that each environment variable is set correctly without leading or trailing spaces. 105 | - If you want an alternative setup, i.e., replacing Ollama with LM studio, laamacpp, etc., check network settings and port mappings as per your configuration requirements. 106 | - A folder 'savefiles' will be created in the folder you run this from. This is where the agents will save their work. 107 | - Star the repo to keep motivation up ;) 108 | 109 | 110 | # Devaloper setup 111 | To get started with the project, follow these steps: 112 | 1. Clone the repository: 113 | ``` 114 | git clone https://github.com/yuriwa/crewai-sheets-ui.git 115 | ``` 116 | 2. Navigate to the project directory: 117 | ``` 118 | cd crewai-sheets-ui 119 | ``` 120 | 3. Install the required dependencies: 121 | ``` 122 | pip install -r requirements.txt 123 | ``` 124 | 4. Create and configure an `.env` file in the project's root directory for storing API keys and other environment variables: 125 | - Rename `example.env`: 126 | ``` 127 | mv example.env .env 128 | ``` 129 | - Edit `.env` with your specific configurations. 130 | 5. Start the application: 131 | ``` 132 | python ./main.py 133 | ``` 134 | 135 | # Usage and first steps. 136 | TODO: 137 | Hopefully it's intuitive enough meanwhile 138 | 139 | # Contributing 140 | Contributions to the crewai-sheets-ui project are welcome. Please ensure to follow the project's code of conduct and submit pull requests for any enhancements or bug fixes. 141 | 142 | # Star History 143 | 144 | 145 | 146 | 147 | 148 | Star History Chart 149 | 150 | 151 | -------------------------------------------------------------------------------- /config/__init__.py: -------------------------------------------------------------------------------- 1 | from .config import AppConfig, ToolsConfig, OllamaConfig 2 | 3 | 4 | -------------------------------------------------------------------------------- /config/config.py: -------------------------------------------------------------------------------- 1 | 2 | import logging 3 | logger = logging.getLogger(__name__) 4 | from utils.import_package_modules import import_package_modules 5 | from langchain.agents.load_tools import load_tools 6 | import langchain_community.utilities as lcutils 7 | import langchain_community.tools as lctools 8 | import crewai_tools 9 | import tools 10 | 11 | class AppConfig: 12 | version = "0.5.3" 13 | name= "crewai-sheets-ui" 14 | template_sheet_url = "https://docs.google.com/spreadsheets/d/1J975Flh82qPjiyUmDE_oKQ2l4iycUq6B3457G5kCD18/copy" 15 | pass 16 | 17 | class ToolsConfig: 18 | # DEFINE MODULES FROM WHICH TO IMPORT TOOLS 19 | # [package, "alias",] 20 | modules_list = [(tools, "tools"),] 21 | callables_list = [load_tools,] # Define specific callables to register e.g. in case they are not callable without specific parameters 22 | integration_dict = {} # Dictionary to which public module members are added 23 | import_package_modules(crewai_tools, modules_list, integration_dict) # Add to modules listAdd all tool modules from crewai_tools 24 | import_package_modules(lctools, modules_list, integration_dict, recursive = True) 25 | import_package_modules(lcutils, modules_list, integration_dict, recursive = True) # Add to modules list all tool modules from langchain_community.tools 26 | pass 27 | 28 | class OllamaConfig: 29 | #patch_stop_words = True 30 | #patch_num_ctx = True 31 | stop_words = [] 32 | 33 | class HuggingFaceConfig: 34 | stop_sequences = ['\nObservation'] 35 | 36 | class GroqConfig: 37 | max_tokens = 1000 38 | stop = [] 39 | def get_rate_limit(model_name:str): 40 | model_rate_dict = { 41 | 'llama2-70b-4096' :15000, 42 | 'mixtral-8x7b-32768':9000, 43 | 'gemma-7b-it' :15000, 44 | 'llama3-70b-8192' :5000, 45 | 'llama3-8b-8192' :12000, 46 | } 47 | if model_name in model_rate_dict: 48 | return model_rate_dict[model_name] 49 | else: 50 | return 5000 51 | -------------------------------------------------------------------------------- /docs/sheets_help.md: -------------------------------------------------------------------------------- 1 | # Frequently Asked Questions (FAQ) 2 | 3 | ### General Questions 4 | 5 | **Q: How do I ensure everything functions smoothly?** 6 | A: Great question! To keep our spreadsheets functioning smoothly, follow these simple habits: 7 | - Do not insert rows. This breaks the formulas. You can achive the same by copying the botom rows then delete and paste special (values only) lower. 8 | - Especially do not insert above the first data row. This will break formulas. 9 | - Only sort columns that are designated for sorting. 10 | - Similarly, only apply filters to designated filtering fields. 11 | - Always use the "Paste Special -> Paste Values Only" option when pasting data. 12 | 13 | - Some parts of the sheet are protected to prevent accidental edits. If you see a warning, please heed it and do not override. 14 | 15 | **Q: Are there plans to develop a dedicated graphical user interface (GUI)?** 16 | A: Absolutely! We're working towards a real GUI that will make your experience even smoother. Stay tuned! 17 | 18 | **Q: What is 'Multiple Select' in the Sheets Crew Menu?** 19 | A: Since Google Sheets doesn't natively support multiple selections in dropdown menus, our 'Multiple Select' tool fills this gap. To use it, simply click on the cell where you want to make multiple selections, then navigate to Sheets Crew -> Multiple Select. 20 | 21 | **Q: What should I do with confusing columns, like 'num_ctx'?** 22 | A: If you encounter confusing columns, a good rule of thumb is to copy values from the row above to maintain consistency. Still puzzled? We're here to help! Feel free to ask your question on our project page: [CrewAI Sheets UI GitHub](https://github.com/yuriwa/crewai-sheets-ui). 23 | 24 | **Q: How can I set the same Language Learning Model (LLM) across all configurations quickly?** 25 | A: To streamline your setup, go to the Models sheet and set your preferred model as 'TRUE' in the Default field. This setting ensures that unless specified otherwise in advanced settings, all tools and functions will use the default model. 26 | 27 | ### Advanced Questions 28 | 29 | **Q: How can I add a new model from Ollama or Huggingface** 30 | A: Adding a new model is straightforward: 31 | - Insert a new row in the Models sheet. 32 | - Copy and paste the 'model:version' exactly as it appears on the Ollama / Huggingface site into the designated field. 33 | - Fill out the remaining fields, and you're all set to use your new model! 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /example.env: -------------------------------------------------------------------------------- 1 | #OPENAI 2 | SECRET_OPENAI_API_KEY = 'REPLACE_WITH_YOUR_OPENAI_API_KEY' 3 | OPENAI_API_KEY = 'REPLACE_WITH_YOUR_OPENAI_API_KEY_ONLY_IF_YOU_HAVE_ADDED_AN_UNCOFIGURABLE_TOOL' 4 | 5 | #SERPER (https://serper.dev/ made a request to search engines (needed for tools)) 6 | SERPER_API_KEY= 'REPLACE_WITH_YOUR_SERPER_API_KEY' 7 | 8 | 9 | #AZURE 10 | AZURE_OPENAI_VERSION="2024-02-15-preview" 11 | AZURE_OPENAI_KEY="REPLACE_WITH_YOUR_AZURE_OPENAI_API_KEY" 12 | AZURE_OPENAI_API_KEY="REPLACE_WITH_YOUR_AZURE_OPENAI_API_KEY" 13 | AZURE_OPENAI_ENDPOINT="ENDPOINT" 14 | #ANTHROPIC 15 | ANTHROPIC_API_KEY = "REPLACE_WITH_YOUR_ANTHROPIC_API_KEY" 16 | 17 | #GROQ 18 | GROQ_API_KEY = "REPLACE_WITH_YOUR_GROQ_API_KEY" 19 | 20 | #HUGGINGFACE 21 | HUGGINGFACEHUB_API_TOKEN="REPLACE_WITH_YOUR_HUGGINGFACEHUB_API_TOKEN" 22 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | import sys 4 | import signal 5 | 6 | from rich.console import Console 7 | from rich.logging import RichHandler 8 | 9 | 10 | ### 11 | console = Console() 12 | logging.basicConfig(level="ERROR", format="%(message)s", datefmt="[%X]", 13 | handlers=[RichHandler(console=console, rich_tracebacks=True)]) 14 | logger = logging.getLogger("rich") 15 | ### 16 | 17 | 18 | ### 19 | os.environ['HAYSTACK_TELEMETRY_ENABLED'] = 'False' # Attmpt to turn off telemetry 20 | os.environ['ANONYMIZED_TELEMETRY'] = 'False' # Disable interpreter telemetry 21 | os.environ['EC_TELEMETRY'] = 'False' # Disable embedchain telemetry 22 | 23 | 24 | ### 25 | def signal_handler(sig, frame): 26 | print("\n\nI received a termination signal. You are the Terminator?! I'll shut down gracefully...\n\n") 27 | sys.exit(0) 28 | 29 | 30 | signal.signal(signal.SIGINT, signal_handler) 31 | signal.signal(signal.SIGTERM, signal_handler) 32 | ### 33 | from rich.table import Table 34 | from textwrap import dedent 35 | from crewai import Crew, Task, Agent, Process 36 | 37 | from utils.agent_crew_llm import get_llm 38 | from utils.tools_mapping import ToolsMapping 39 | from utils.cli_parser import get_parser 40 | from utils.helpers import load_env, is_valid_google_sheets_url, get_sheet_url_from_user 41 | from utils import Sheets, helpers 42 | 43 | import pandas as pd 44 | import sentry_sdk 45 | from config.config import AppConfig 46 | 47 | 48 | def create_agents_from_df(row, models_df=None, tools_df=None): 49 | def get_agent_tools(tools_string): 50 | tool_names = [tool.strip() for tool in tools_string.split(',')] 51 | tools_mapping = ToolsMapping(tools_df, models_df) # Get the ToolsMapping instance 52 | tools_dict = tools_mapping.get_tools() # Get the dictionary of tools from the instance 53 | return [tools_dict[tool] for tool in tool_names if tool in tools_dict] 54 | 55 | role = row.get('Agent Role', "Assistant") 56 | goal = row.get('Goal', "To assist the human in their tasks") 57 | model_name = row.get('Model Name', 'gpt-4-turbo-preview').strip() 58 | backstory = row.get('Backstory', "...") 59 | temperature = row.get('Temperature', 0.8) 60 | max_iter = row.get('Max_iter', 15) 61 | verbose = row.get('Verbose', True) 62 | memory = row.get('Memory', True) 63 | tools_string = row.get('Tools') 64 | tools = get_agent_tools(tools_string) 65 | allow_delegation = row.get('Allow delegation', False) 66 | 67 | # Retrieve Agent model details 68 | model_details = models_df[ 69 | models_df['Model'].str.strip() == model_name] # Filter the models dataframe for the specific model 70 | 71 | if model_details.empty: 72 | llm = None 73 | raise ValueError(f"Failed to retrieve or initialize the language model for {model_name}") 74 | else: 75 | # Retrieve each attribute, ensuring it exists and is not NaN; otherwise, default to None 76 | num_ctx = int(model_details['Context size (local only)'].iloc[ 77 | 0]) if 'Context size (local only)' in model_details.columns and not pd.isna( 78 | model_details['Context size (local only)'].iloc[0]) else None 79 | provider = str(model_details['Provider'].iloc[0]) if 'Provider' in model_details.columns and not pd.isna( 80 | model_details['Provider'].iloc[0]) else None 81 | base_url = str(model_details['base_url'].iloc[0]) if 'base_url' in model_details.columns and not pd.isna( 82 | model_details['base_url'].iloc[0]) else None 83 | deployment = str(model_details['Deployment'].iloc[0]) if 'Deployment' in model_details.columns and not pd.isna( 84 | model_details['Deployment'].iloc[0]) else None 85 | 86 | llm = get_llm( 87 | model_name=model_name, 88 | temperature=temperature, 89 | num_ctx=num_ctx, 90 | provider=provider, 91 | base_url=base_url, 92 | deployment=deployment, 93 | ) 94 | 95 | # Retrieve function calling model details 96 | function_calling_model_name = row.get('Function Calling Model', model_name) 97 | if isinstance(function_calling_model_name, str): 98 | # print(function_calling_model_name) 99 | function_calling_model_name = function_calling_model_name.strip() 100 | function_calling_model_details = models_df[models_df['Model'].str.strip() == function_calling_model_name] 101 | else: 102 | function_calling_model_details = None 103 | 104 | if function_calling_model_details is None or function_calling_model_details.empty: 105 | function_calling_llm = llm 106 | else: 107 | num_ctx = int(function_calling_model_details['Context size (local only)'].iloc[ 108 | 0]) if 'Context size (local only)' in function_calling_model_details.columns and not pd.isna( 109 | function_calling_model_details['Context size (local only)'].iloc[0]) else None 110 | provider = function_calling_model_details['Provider'].iloc[ 111 | 0] if 'Provider' in function_calling_model_details.columns and not pd.isna( 112 | function_calling_model_details['Provider'].iloc[0]) else None 113 | base_url = function_calling_model_details['base_url'].iloc[ 114 | 0] if 'base_url' in function_calling_model_details.columns and not pd.isna( 115 | function_calling_model_details['base_url'].iloc[0]) else None 116 | deployment = function_calling_model_details['Deployment'].iloc[ 117 | 0] if 'Deployment' in function_calling_model_details.columns and not pd.isna( 118 | function_calling_model_details['Deployment'].iloc[0]) else None 119 | 120 | function_calling_llm = get_llm( 121 | model_name=function_calling_model_name, 122 | temperature=temperature, 123 | num_ctx=num_ctx, 124 | provider=provider, 125 | base_url=base_url, 126 | deployment=deployment, 127 | ) 128 | 129 | agent_config = { 130 | # agent_executor: #An instance of the CrewAgentExecutor class. 131 | 'role': role, 132 | 'goal': goal, 133 | 'backstory': backstory, 134 | 'allow_delegation': allow_delegation, # Whether the agent is allowed to delegate tasks to other agents. 135 | 'verbose': verbose, # Whether the agent execution should be in verbose mode. 136 | 'tools': tools, # Tools at agents disposal 137 | 'memory': memory, # Whether the agent should have memory or not. 138 | 'max_iter': max_iter, 139 | # TODO: Remove hardcoding #Maximum number of iterations for an agent to execute a task. 140 | 'llm': llm, # The language model that will run the agent. 141 | 'function_calling_llm': function_calling_llm 142 | # The language model that will the tool calling for this agent, it overrides the crew function_calling_llm. 143 | # step_callback: #Callback to be executed after each step of the agent execution. 144 | # callbacks: #A list of callback functions from the langchain library that are triggered during the agent's execution process 145 | } 146 | if llm is None: 147 | print(f"I couldn't manage to create an llm model for the agent. {role}. The model was supposed to be {model_name}.") 148 | print(f"Please check the api keys and model name and the configuration in the sheet. Exiting...") 149 | sys.exit(0) 150 | else: 151 | return Agent(config=agent_config) 152 | 153 | 154 | 155 | def get_agent_by_role(agents, desired_role): 156 | return next((agent for agent in agents if agent.role == desired_role), None) 157 | 158 | 159 | def create_tasks_from_df(row, assignment, created_agents, **kwargs): 160 | description = row['Instructions'].replace('{assignment}', assignment) 161 | desired_role = row['Agent'] 162 | 163 | return Task( 164 | description=dedent(description), 165 | expected_output=row['Expected Output'], 166 | agent=get_agent_by_role(created_agents, desired_role) 167 | ) 168 | 169 | 170 | def create_crew(created_agents, created_tasks, crew_df): 171 | # Embedding model (Memory) 172 | memory = crew_df['Memory'][0] 173 | embedding_model = crew_df['Embedding model'].get(0) 174 | 175 | if embedding_model is None or pd.isna(embedding_model): 176 | logger.info("No embedding model for crew specified in the sheet. Turning off memory.") 177 | deployment_name = None 178 | provider = None 179 | base_url = None 180 | memory = False 181 | embedder_config = None 182 | else: 183 | deployment_name = models_df.loc[models_df['Model'] == embedding_model, 'Deployment'].values[0] 184 | provider = models_df.loc[models_df['Model'] == embedding_model, 'Provider'].values[0] 185 | base_url = models_df.loc[models_df['Model'] == embedding_model, 'base_url'].values[0] 186 | 187 | # Create provider specific congig and load proveder specific ENV variables if it can't be avoided 188 | embedder_config = { 189 | "model": embedding_model, 190 | } 191 | 192 | if provider == 'azure-openai': 193 | embedder_config['deployment_name'] = deployment_name # Set azure specific config 194 | # os.environ["AZURE_OPENAI_DEPLOYMENT"] = deployment_name #Wrokarond since azure 195 | os.environ["OPENAI_API_KEY"] = os.environ["AZURE_OPENAI_KEY"] 196 | 197 | if provider == 'openai': 198 | embedder_config['api_key'] = os.environ.get("SECRET_OPENAI_API_KEY") 199 | os.environ["OPENAI_BASE_URL"] = "https://api.openai.com/v1" 200 | 201 | if provider == 'ollama': 202 | if base_url is not None: 203 | embedder_config['base_url'] = base_url 204 | 205 | elif embedder_config is not None : # Any other openai compatible e.g. ollama or llama-cpp 206 | provider = 'openai' 207 | api_key = 'NA' 208 | embedder_config['base_url'] = base_url 209 | embedder_config['api_key'] = api_key 210 | 211 | # Groq doesn't have an embedder 212 | 213 | # Manager LLM 214 | manager_model = crew_df['Manager LLM'][0] 215 | manager_provider = models_df.loc[models_df['Model'] == manager_model, 'Provider'].values[0] 216 | manager_temperature = crew_df['t'][0] 217 | manager_num_ctx = crew_df['num_ctx'][0] 218 | manager_base_url = models_df.loc[models_df['Model'] == manager_model, 'base_url'].values[0] 219 | manager_deployment = models_df.loc[models_df['Model'] == manager_model, 'Deployment'].values[0] 220 | 221 | if manager_model and manager_provider is not None: 222 | manager_llm = get_llm( 223 | model_name=manager_model, 224 | temperature=manager_temperature, 225 | num_ctx=manager_num_ctx, 226 | provider=manager_provider, 227 | base_url=manager_base_url, 228 | deployment=manager_deployment 229 | ) 230 | 231 | verbose = crew_df['Verbose'][0] 232 | process = Process.hierarchical if crew_df['Process'][0] == 'hierarchical' else Process.sequential 233 | 234 | return Crew( 235 | agents=created_agents, 236 | tasks=created_tasks, 237 | verbose=verbose, 238 | process=process, 239 | memory=memory, 240 | manager_llm=manager_llm, 241 | embedder={ 242 | "provider": provider, 243 | "config": embedder_config 244 | } 245 | ) 246 | 247 | 248 | if __name__ == "__main__": 249 | release = f"{AppConfig.name}@{AppConfig.version}" 250 | if os.environ.get("CREWAI_SHEETS_SENRY") != "False": 251 | sentry_sdk.init( 252 | dsn="https://fc662aa323fcc1629fb9ea7713f63137@o4507186870157312.ingest.de.sentry.io/4507186878414928", 253 | traces_sample_rate=1.0, 254 | profiles_sample_rate=1.0, 255 | release=release, 256 | ) 257 | helpers.greetings_print() 258 | args = get_parser() 259 | log_level = args.loglevel.upper() if hasattr(args, 'loglevel') else "ERROR" 260 | logger.setLevel(log_level) 261 | 262 | load_env(args.env_path, ["OPENAI_API_KEY", ]) 263 | 264 | if hasattr(args, "sheet_url") and args.sheet_url and is_valid_google_sheets_url(args.sheet_url): 265 | sheet_url = args.sheet_url 266 | else: 267 | sheet_url = get_sheet_url_from_user() 268 | 269 | # Define a function to handle termination signals 270 | terminal_width = console.width 271 | terminal_width = max(terminal_width, 120) 272 | 273 | # Enter main process 274 | agents_df, tasks_df, crew_df, models_df, tools_df = Sheets.parse_table(sheet_url) 275 | helpers.after_read_sheet_print(agents_df, tasks_df) # Print overview of agents and tasks 276 | 277 | # Create Agents 278 | agents_df['crewAIAgent'] = agents_df.apply( 279 | lambda row: create_agents_from_df(row, models_df=models_df, tools_df=tools_df), axis=1) 280 | created_agents = agents_df['crewAIAgent'].tolist() 281 | 282 | # Create Tasks 283 | assignment = crew_df['Assignment'][0] 284 | tasks_df['crewAITask'] = tasks_df.apply(lambda row: create_tasks_from_df(row, assignment, created_agents), axis=1) 285 | created_tasks = tasks_df['crewAITask'].tolist() 286 | 287 | # Creating crew 288 | crew = create_crew(created_agents, created_tasks, crew_df) 289 | console.print("[green]I've created the crew for you. Let's start working on these tasks! :rocket: [/green]") 290 | 291 | try: 292 | results = crew.kickoff() 293 | except Exception as e: 294 | console.print(f"[red]I'm sorry, I couldn't complete the tasks :( Here's the error I encountered: {e}") 295 | sys.exit(0) 296 | 297 | # Create a table for results 298 | result_table = Table(show_header=True, header_style="bold magenta") 299 | result_table.add_column("Here are the results, see you soon =) ", style="green", width=terminal_width) 300 | 301 | result_table.add_row(str(results)) 302 | console.print(result_table) 303 | console.print("[bold green]\n\n") 304 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "crewai-sheets-ui" 3 | version = "0.0.2" 4 | description = "Version 0.0.2 of crewai-sheets-ui" 5 | authors = ["yuriwa"] 6 | license = "MIT" 7 | readme = "README.md" 8 | 9 | [tool.poetry.dependencies] 10 | python = ">=3.10,<3.12" 11 | crewai = "^0.28.8" 12 | crewai-tools = {extras = ["tools"], version = "^0.1.7"} 13 | open-interpreter = "^0.2.4" 14 | duckduckgo-search = "^5.3.0" 15 | pandas = "^2.2.2" 16 | ollama = "^0.1.8" 17 | langchain-community = "^0.0.32" 18 | rich = "^13.7.1" 19 | langchain-anthropic = "^0.1.8" 20 | restrictedpython = "^7.1" 21 | langchain-groq = "^0.1.3" 22 | wikipedia = "^1.4.0" 23 | 24 | 25 | [build-system] 26 | requires = ["poetry-core"] 27 | build-backend = "poetry.core.masonry.api" 28 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | crewai 2 | crewai[tools] 3 | open-interpreter 4 | duckduckgo-search 5 | pandas 6 | ollama 7 | langchain_community 8 | langchain-anthropic 9 | rich 10 | -------------------------------------------------------------------------------- /tools/__init__.py: -------------------------------------------------------------------------------- 1 | from .folder_tool import FolderTool 2 | from .interpreter_tool import CLITool 3 | from .bs_tool import BSharpCodeTool 4 | from .file_append_tool import AppendFileTool 5 | from .file_count_lines import FileCountLinesTool 6 | from .file_create_tool import CreateFileTool 7 | from .file_edit_tool import EditFileTool 8 | from .file_line_read_tool import LineReadFileTool 9 | from .create_draft_tool import CreateDraftTool -------------------------------------------------------------------------------- /tools/bs_tool.py: -------------------------------------------------------------------------------- 1 | import logging 2 | logger = logging.getLogger(__name__) 3 | logger.debug(f"Entered {__file__}") 4 | from rich.console import Console 5 | from rich.syntax import Syntax 6 | from pydantic.v1 import BaseModel, Field 7 | from langchain.tools import tool 8 | from pygments.lexer import RegexLexer 9 | from pygments.token import Token 10 | from typing import Type, Any 11 | from crewai_tools import BaseTool 12 | 13 | class BSharpLexer(RegexLexer): 14 | """ 15 | Custom lexer for the BSharp language, defining syntax highlighting rules using Pygments. 16 | """ 17 | name = "BSharp" 18 | aliases = ['bsharp'] 19 | filenames = ['*.bs'] 20 | 21 | tokens = { 22 | 'root': [ 23 | (r'\bobject\b', Token.Keyword), 24 | (r'\bprocedure\b', Token.Keyword.Declaration), 25 | (r'\bif\b', Token.Keyword), 26 | (r'\berror\b', Token.Keyword), 27 | (r'[a-zA-Z_][a-zA-Z0-9_]*', Token.Name), 28 | (r':', Token.Punctuation), 29 | (r'[{}(),;]', Token.Punctuation), 30 | (r'".*?"', Token.String), 31 | (r'\d+', Token.Number), 32 | (r'#.*$', Token.Comment), 33 | (r'\s+', Token.Text), 34 | ] 35 | } 36 | 37 | class BSharpCodeToolSchema(BaseModel): 38 | """ 39 | Input schema for BSharpCodeTool, specifying the required parameters for code highlighting. 40 | """ 41 | code: str = Field(..., description="BSharp code to be highlighted and displayed.") 42 | 43 | class BSharpCodeTool(BaseTool): 44 | """ 45 | Tool to display BSharp code with syntax highlighting using the Rich library and a custom Pygments lexer. 46 | """ 47 | name: str = "BSharp Code Output Tool" 48 | description: str = "Tool to display BSharp code with syntax highlighting. Takes in one parameter: 'code' - the BSharp code to be highlighted and displayed." 49 | args_schema: Type[BaseModel] = BSharpCodeToolSchema 50 | 51 | def __init__(self, **kwargs): 52 | super().__init__(**kwargs) 53 | self._generate_description() # Call to the inherited method to set the initial description 54 | 55 | def _run(self, **kwargs: Any) -> Any: 56 | code = kwargs.get('code') 57 | if not code: 58 | return "Error: No code provided for highlighting. Please provide BSharp code as the 'code' parameter." 59 | 60 | console = Console() 61 | syntax = Syntax(code, lexer=BSharpLexer(), theme="github-dark", line_numbers=True) 62 | console.print(syntax) 63 | return code 64 | 65 | # Example usage 66 | if __name__ == "__main__": 67 | tool = BSharpCodeTool() 68 | tool.run(code=''' 69 | object Person 70 | id:Integer 71 | name:String 72 | 73 | procedure AddPerson(PersonDTO, out Integer) 74 | if People.ContainsKey(PersonDTO.id) 75 | error[InvalidPersonId] "Person with ID already exists." 76 | ''') 77 | -------------------------------------------------------------------------------- /tools/create_draft_tool.py: -------------------------------------------------------------------------------- 1 | from langchain_community.agent_toolkits import GmailToolkit 2 | from langchain_community.tools.gmail.create_draft import GmailCreateDraft 3 | from langchain.tools import tool 4 | 5 | class CreateDraftTool(): 6 | @tool("Create Draft") 7 | def create_draft(data): 8 | """ 9 | Useful to create an email draft. 10 | The input to this tool should be a pipe (|) separated text 11 | of length 3 (three), representing who to send the email to, 12 | the subject of the email and the actual message. 13 | For example, `lorem@ipsum.com|Nice To Meet You|Hey it was great to meet you.`. 14 | """ 15 | email, subject, message = data.split('|') 16 | gmail = GmailToolkit() 17 | draft = GmailCreateDraft(api_resource=gmail.api_resource) 18 | resutl = draft({ 19 | 'to': [email], 20 | 'subject': subject, 21 | 'message': message 22 | }) 23 | return f"\nDraft created: {resutl}\n" 24 | 25 | 26 | -------------------------------------------------------------------------------- /tools/file_append_tool.py: -------------------------------------------------------------------------------- 1 | import os 2 | import logging 3 | from typing import Optional, Type, Any 4 | logger = logging.getLogger(__name__) 5 | logger.debug(f"Entered {__file__}") 6 | 7 | from pydantic.v1 import BaseModel, Field, validator 8 | from crewai_tools import BaseTool 9 | 10 | class FixedFileToolSchema(BaseModel): 11 | """Input for AppendFileTool.""" 12 | 13 | 14 | class AppendFileToolSchema(FixedFileToolSchema): 15 | """Input for appending text to a file.""" 16 | file_path: str = Field(..., description="Manadatory file full path to append the text.") 17 | append_text: str = Field(..., description="Mandatory text to be appended to the file.") 18 | 19 | 20 | class AppendFileTool(BaseTool): 21 | name: str = "Append text to a file" 22 | description: str = "A tool that can be used to append text to a file." 23 | args_schema: Type[BaseModel] = AppendFileToolSchema 24 | file_path: Optional[str] = None 25 | append_text: Optional[str] = None 26 | 27 | def __init__(self, file_path: Optional[str] = None, append_text: Optional[str] = None, **kwargs): 28 | super().__init__(**kwargs) 29 | 30 | if file_path is not None: 31 | self.file_path = file_path 32 | self.append_text = append_text 33 | self.description = f"A tool that can be used to append text to {file_path}." 34 | self.args_schema = FixedFileToolSchema 35 | self._generate_description() 36 | 37 | def _run(self, 38 | **kwargs: Any, 39 | ) -> Any: 40 | try: 41 | file_path = kwargs.get('file_path', self.file_path) 42 | append_text = kwargs.get('append_text', self.append_text) 43 | with open(file_path, 'a') as file: 44 | file.write(append_text + '\n') 45 | return "Text appended successfully." 46 | except Exception as e: 47 | return f"Failed to append text: {e}" 48 | -------------------------------------------------------------------------------- /tools/file_count_lines.py: -------------------------------------------------------------------------------- 1 | import os 2 | import logging 3 | logger = logging.getLogger(__name__) 4 | logger.debug(f"Entered {__file__}") 5 | from langchain.tools import tool 6 | from typing import Optional, Type, Any 7 | from pydantic.v1 import BaseModel, Field, validator 8 | from crewai_tools import BaseTool 9 | 10 | class FixedFileToolSchema(BaseModel): 11 | """Input for FileCountLinesTool.""" 12 | 13 | class FileCountLinesToolSchema(FixedFileToolSchema): 14 | """Input for FileCountLinesTool""" 15 | file_path: str = Field(..., description="The path to the file.") 16 | 17 | class FileCountLinesTool(BaseTool): 18 | name: str = "Count a file's lines" 19 | description: str = "A tool that can be used to count the number of lines in a file." 20 | args_schema: Type[BaseModel] = FileCountLinesToolSchema 21 | file_path: Optional[str] = None 22 | 23 | def __init__(self, file_path: Optional[str] = None, **kwargs): 24 | super().__init__(**kwargs) 25 | if file_path is not None: 26 | self.file_path = file_path 27 | self.description = f"A tool that can be used to count the number of lines in {file_path}." 28 | self.args_schema = FixedFileToolSchema 29 | self._generate_description() 30 | 31 | def _run( 32 | self, 33 | **kwargs: Any, 34 | ) -> Any: 35 | file_path = kwargs.get('file_path', self.file_path) 36 | try: 37 | with open(file_path, 'r') as file: 38 | lines = file.readlines() 39 | return f"Total lines: {len(lines)}" 40 | except Exception as e: 41 | return f"Error reading file: {e}" -------------------------------------------------------------------------------- /tools/file_create_tool.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from typing import Optional, Type, Any 3 | logger = logging.getLogger(__name__) 4 | 5 | from pydantic.v1 import BaseModel, Field 6 | from crewai_tools import BaseTool 7 | 8 | class FixedFileToolSchema(BaseModel): 9 | """Input for FileTool.""" 10 | pass 11 | 12 | class CreateFileSchema(FixedFileToolSchema): 13 | """Input for CreateFileTool.""" 14 | file_path: str = Field(..., description="The path to the file.") 15 | 16 | class CreateFileTool(BaseTool): 17 | name: str = "Create a file" 18 | description: str = "A tool that's used to Create a file." 19 | args_schema: Type[BaseModel] = CreateFileSchema 20 | file_path: Optional[str] = None 21 | 22 | def __init__(self, file_path: Optional[str] = None, **kwargs): 23 | super().__init__(**kwargs) 24 | 25 | if file_path is not None: 26 | self.file_path = file_path 27 | self.description = f"A tool that's used to create a file at {file_path}." 28 | self.args_schema = FixedFileToolSchema 29 | self._generate_description() 30 | 31 | def _run( 32 | self, 33 | **kwargs: Any, 34 | )-> Any: 35 | try: 36 | file_path = kwargs.get('file_path', self.file_path) 37 | with open(file_path, 'x') as file: 38 | return "File created successfully." 39 | except FileExistsError: 40 | return "File already exists." 41 | -------------------------------------------------------------------------------- /tools/file_edit_tool.py: -------------------------------------------------------------------------------- 1 | import os 2 | import logging 3 | logger = logging.getLogger(__name__) 4 | from langchain.tools import tool 5 | from typing import Optional, Type, Any 6 | from pydantic.v1 import BaseModel, Field, validator 7 | from crewai_tools import BaseTool 8 | 9 | 10 | 11 | 12 | class FixedFileToolSchema(BaseModel): 13 | """Input for EditFileTool.""" 14 | 15 | 16 | class FileEditToolSchema(FixedFileToolSchema): 17 | """Input for EditFileTool.""" 18 | file_path: str = Field(..., description="Mandatory file full path to edit the file") 19 | line_number: int = Field(..., description="Mandatory line number (1-based) to edit.") 20 | expected_text: str = Field(..., description="Mandatory text to be replaced on the specified line.") 21 | new_text: str = Field(..., description="Manadatory new text to replace the expected text.") 22 | 23 | class EditFileTool(BaseTool): 24 | name: str = "Edit a file's line" 25 | description: str = "A tool that can be used to edit a specific line in a file." 26 | args_schema: Type[BaseModel] = FileEditToolSchema 27 | file_path: Optional[str] = None 28 | line_number: Optional[int] = None 29 | expected_text: Optional[str] = None 30 | new_text: Optional[str] = None 31 | 32 | def __init__(self, file_path: Optional[str] = None, 33 | line_number: Optional[int] = None, 34 | expected_text: Optional[str] = None, 35 | new_text: Optional[str] = None ,**kwargs): 36 | super().__init__(**kwargs) 37 | 38 | if file_path is not None or line_number is not None or expected_text is not None or new_text is not None: 39 | self.file_path = file_path 40 | self.line_number = line_number 41 | self.expected_text = expected_text 42 | self.new_text = new_text 43 | self.description = f"A tool that can be used to edit a specific line in {file_path}." 44 | self.args_schema = FixedFileToolSchema 45 | self._generate_description() 46 | 47 | def _run( 48 | self, 49 | **kwargs: Any, 50 | ) -> Any: 51 | file_path = kwargs.get('file_path', self.file_path) 52 | line_number = kwargs.get('line_number', self.line_number) 53 | expected_text = kwargs.get('expected_text', self.expected_text) 54 | new_text = kwargs.get('new_text', self.new_text) 55 | with open(file_path, 'r') as file: 56 | lines = file.readlines() 57 | # Check if the line number is within the file's range 58 | if not 1 <= line_number <= len(lines): 59 | return f"I made an error: Line number {line_number} is out of the file's range. The file has {len(lines)} lines. The first line is line 1." 60 | 61 | # Check if the expected text matches the current line content 62 | current_line = lines[line_number - 1].rstrip("\n") 63 | 64 | if expected_text is not None and current_line != expected_text: 65 | return f"I made an Error: Expected text does not match the text on line {line_number}." 66 | 67 | # Replace the line with new text 68 | lines[line_number - 1] = new_text + '\n' 69 | 70 | # Write the updated lines back to the file directly within this method 71 | try: 72 | with open(file_path, 'w') as file: 73 | file.writelines(lines) 74 | except Exception as e: 75 | return f"There was an eeror writing to file {file_path}: {e}" 76 | 77 | return "Line edited successfully." 78 | 79 | 80 | 81 | -------------------------------------------------------------------------------- /tools/file_line_read_tool.py: -------------------------------------------------------------------------------- 1 | import os 2 | import logging 3 | logger = logging.getLogger(__name__) 4 | from langchain.tools import tool 5 | from typing import Optional, Type, Any 6 | from pydantic.v1 import BaseModel, Field, validator 7 | from crewai_tools import BaseTool 8 | 9 | 10 | class FixedFileToolSchema(BaseModel): 11 | """Input for LineReadFileTool""" 12 | 13 | 14 | class LineReadFileToolSchema(FixedFileToolSchema): 15 | """Input for LineReadFileTool""" 16 | file_path: str = Field(..., description="Mandatory file full path to read the file") 17 | line_number: int = Field(..., description="Manadatory line number (1-based) to start reading from.") 18 | num_lines: Optional[int] = Field(..., description="Optional number of lines to read from the starting line. If not specified, reads all lines starting from `line_number`.") 19 | 20 | class LineReadFileTool(BaseTool): 21 | name: str = "Read a file's content by line number" 22 | description: str = "A tool that can be used to read a file's content by line number." 23 | args_schema: Type[BaseModel] = LineReadFileToolSchema 24 | file_path: Optional[str] = None 25 | line_number: Optional[int] = None 26 | num_lines: Optional[int] = None 27 | 28 | def __init__(self, file_path: Optional[str] = None, line_number: Optional[int] = None, num_lines: Optional[int] = None, **kwargs): 29 | super().__init__(**kwargs) 30 | if file_path is not None and line_number is not None: 31 | self.file_path = file_path 32 | self.line_number = line_number 33 | self.num_lines = num_lines 34 | self.description = f"A tool that can be used to read {file_path}'s content by line number." 35 | self.args_schema = FixedFileToolSchema 36 | self._generate_description() 37 | 38 | def _run( 39 | self, 40 | **kwargs: Any, 41 | ) -> Any: 42 | file_path = kwargs.get('file_path', self.file_path) 43 | line_number = kwargs.get('line_number', self.line_number) 44 | num_lines = kwargs.get('num_lines', self.num_lines) 45 | if num_lines is not None: 46 | if num_lines == 0: 47 | num_lines = None # Normalize zero to None to indicate "read all lines" 48 | elif num_lines < 1: 49 | return "I made a mistake, I forgot that number of lines has to be positive." 50 | # Ensure line_number starts at least from 1 51 | if line_number < 1: 52 | return "I made a mistake, I forgot that the first line is 1." 53 | 54 | with open(file_path, 'r') as file: 55 | lines = file.readlines() 56 | 57 | # Validate line_number to ensure it's within the range of the file's line count. 58 | if line_number > len(lines): 59 | return f"I made a mistake: Line number {line_number} is out of the file's range." 60 | 61 | # Calculate the end index for slicing lines; handle case where num_lines is None 62 | end_index = (line_number - 1) + num_lines if num_lines else len(lines) 63 | selected_lines = lines[line_number - 1:end_index] # Adjust for zero-based index 64 | 65 | if not selected_lines: 66 | return "No lines found starting from the specified line number." 67 | 68 | # Format output to include line numbers with their respective contents 69 | content = ''.join([f"{idx + line_number}: {line}" for idx, line in enumerate(selected_lines)]) 70 | return content 71 | -------------------------------------------------------------------------------- /tools/file_tool_(depricated).py: -------------------------------------------------------------------------------- 1 | import logging 2 | logger = logging.getLogger(__name__) 3 | logger.debug(f"Entered {__file__}") 4 | from langchain.tools import tool 5 | from typing import Optional, Type, Any 6 | from pydantic.v1 import BaseModel, Field, validator 7 | from crewai_tools import BaseTool 8 | from enum import Enum 9 | 10 | #import traceback 11 | #import sys 12 | 13 | 14 | 15 | 16 | class FixedFileToolSchema(BaseModel): 17 | """Input for FileTool.""" 18 | pass 19 | 20 | class FileToolSchema(FixedFileToolSchema): 21 | """Input for FileTool.""" 22 | file_path: str = Field(..., description="The path to the file.") 23 | operation: str = Field(..., description="The operation to perform on the file.") 24 | line_number: int = Field(None, description="The line number to start reading from. Line numbers are 1-based.") 25 | num_lines: Optional[int] = Field(None, description="The number of lines to read starting from line_number.") 26 | expected_text: str = Field(None, description="The text expected to be on the specified line for editing.") 27 | new_text: str = Field(None, description="The new text to replace the existing text on the specified line.") 28 | append_text: str = Field(None, description="The text to be appended to the file.") 29 | 30 | @validator('file_path') 31 | def check_file_exists(cls, v, values): 32 | import os 33 | if values['operation'] != "create" and not os.path.exists(v): 34 | raise ValueError("file must exist for the specified operation") 35 | return v 36 | 37 | class FileTool(BaseTool): 38 | name: str = "General purpose file management tool" 39 | description: str = "Manage 'append', 'edit', 'create', 'count_lines', 'read', 'create' file operations" 40 | args_schema: Type[BaseModel] = FileToolSchema 41 | file_path: Optional[str] = None 42 | 43 | def __init__(self, file_path: Optional[str] = None, **kwargs): 44 | # print("Entering FileTool constructor...") 45 | # print(f"file_path: {file_path}") 46 | # print(f"kwargs: {kwargs}") 47 | super().__init__(**kwargs) 48 | self.description = """ 49 | Supported Operations: 50 | - 'append': Adds specified text to the end of the file. 51 | Parameters: 52 | - file_path: Path to the file. 53 | - append_text: Text to be appended. 54 | 55 | - 'edit': Modifies a specific line in the file, provided the existing content matches the expected input. 56 | Parameters: 57 | - file_path: Path to the file. 58 | - line_number: Line number to edit. 59 | - expected_text: The text currently expected on the line. 60 | - new_text: The new text to replace the existing line content. 61 | 62 | - 'create': Generates a new file or gives an error if the file already exists. 63 | Parameters: 64 | - file_path: Path to the file where the new file will be created. 65 | - append_text: Text to be appended. 66 | 67 | - 'count_lines': Calculates the total number of lines in the file. 68 | Parameters: 69 | - file_path: Path to the file. 70 | 71 | - 'get_line': Retrieves the content of a specific line based on line number. 72 | Parameters: 73 | - file_path: Path to the file. 74 | - line_number: The line number whose content is to be retrieved. 75 | 76 | - 'read': Extracts a segment of the file starting from a specified line and covering a defined number of subsequent lines. 77 | Parameters: 78 | - file_path: Path to the file. 79 | - line_number: The starting line number from where to begin reading. 80 | - num_lines: The number of lines to read from the starting line. If not specified, reads all lines starting from `line_number`. 81 | Line numbers are 1-based, meaning the first line is line 1. 82 | """ 83 | if file_path is not None: 84 | self.file_path = file_path 85 | else: 86 | self._generate_description() 87 | 88 | def _run(self, **kwargs: Any) -> Any: 89 | #self.args_schema = FixedFileToolSchema *args:Any 90 | try: 91 | operation = kwargs.get('operation') 92 | #print(f"Operation: {operation}, Type:{type(operation)}") #Debug 93 | #for each in kwargs print th key and value and type 94 | # print("Printing kwargs...") 95 | # for key, value in kwargs.items(): 96 | # print(f"{key}: {value}, Type: {type(value)}") #Debug 97 | # print("Printing args...") 98 | # for each in args: 99 | # print(f"{each}") 100 | #traceback.print_stack(file=sys.stdout) 101 | #print args 102 | #print(f"args: {args}") #Debug 103 | #print(f"kwargs: {kwargs}") #Debug 104 | if operation == 'append': 105 | return self.__append_text(**kwargs) 106 | elif operation == 'edit': 107 | return self.__edit_line(**kwargs) 108 | elif operation == 'create': 109 | return self.__create_file(**kwargs) 110 | elif operation == 'count_lines': 111 | return self.__count_lines(**kwargs) 112 | elif operation == 'get_line': 113 | return self.__get_line(**kwargs) 114 | elif operation == 'read': 115 | return self.__read_file(**kwargs) 116 | else: 117 | self.description = f"I made an invalid operation '{operation}'. Valid operations are: append, edit, create, count_lines, get_line, read." 118 | #traceback.print_stack(file=sys.stderr) 119 | self._generate_description() 120 | return self.description 121 | except Exception as e: 122 | error_message = f"Error processing operation '{kwargs.get('operation', 'unknown')}'. Exception type: {type(e).__name__}, Exception info: {e}, Object state: {self.__dict__}" 123 | return error_message 124 | 125 | 126 | def __append_text(self, file_path, append_text=None, **kwargs): 127 | # print(f"file_path: {file_path}") 128 | # print(f"append_text: {append_text}") 129 | # print(f"kwargs: {kwargs}") 130 | try: 131 | with open(file_path, 'a') as file: 132 | file.write(append_text + '\n') 133 | return "Text appended successfully." 134 | except Exception as e: 135 | return f"Failed to append text: {e}" 136 | 137 | def __edit_line(self, file_path, line_number, expected_text, new_text, **kwargs): 138 | """Edits a specific line in the file if the expected text matches.""" 139 | lines = self.__read_file_lines(file_path) 140 | if not 1 <= line_number <= len(lines): 141 | return f"I made an error: Line number {line_number} is out of the file's range. The file has {len(lines)} lines. The first line is line 1." 142 | # Check if the expected text matches the current line content 143 | current_line = lines[line_number - 1].rstrip("\n") 144 | if expected_text is not None and current_line != expected_text: 145 | return f"I made an Error: Expected text does not match the text on line {line_number}." 146 | # Replace the line with new text 147 | lines[line_number - 1] = new_text + '\n' 148 | # Write the updated lines back to the file directly within this method 149 | with open(file_path, 'w') as file: 150 | file.writelines(lines) 151 | return "Line edited successfully." 152 | 153 | def __read_file(self, file_path, line_number=1, num_lines=None, **kwargs): 154 | """ 155 | Reads a specific number of lines starting from a given line number from the file at file_path. 156 | Parameters: 157 | file_path: The path to the file to read. 158 | line_number (optional): The line number from which to start reading. Defaults to 1. 159 | num_lines (optional): The number of lines to read starting from line_number. If None, reads to the end of the file. 160 | Returns: 161 | line numbers and their contents if successful. 162 | Error message if an input constraint is violated or an error occurs. 163 | """ 164 | # Normalize num_lines to handle 0 as 'read all lines' and enforce positive integers 165 | if num_lines is not None: 166 | if num_lines == 0: 167 | num_lines = None # Normalize zero to None to indicate "read all lines" 168 | elif num_lines < 1: 169 | return "I made a mistake, I forgot that number of lines has to be positive." 170 | 171 | # Ensure line_number starts at least from 1 172 | if line_number < 1: 173 | return "I made a mistake, I forgot that the first line is 1." 174 | 175 | lines = self.__read_file_lines(file_path) 176 | 177 | 178 | # Validate line_number to ensure it's within the range of the file's line count. 179 | if line_number > len(lines): 180 | return f"I made a mistake: Line number {line_number} is out of the file's range." 181 | 182 | # Calculate the end index for slicing lines; handle case where num_lines is None 183 | end_index = (line_number - 1) + num_lines if num_lines else len(lines) 184 | selected_lines = lines[line_number - 1:end_index] # Adjust for zero-based index 185 | 186 | if not selected_lines: 187 | return "No lines found starting from the specified line number." 188 | 189 | # Format output to include line numbers with their respective contents 190 | content = ''.join([f"{idx + line_number}: {line}" for idx, line in enumerate(selected_lines)]) 191 | return content 192 | 193 | def __create_file(self, file_path, **kwargs): 194 | """Creates a new file or overwrites an existing one.""" 195 | # print(f"__create file_path: {file_path}") 196 | # print(f"kwargs: {kwargs}") 197 | # print(f"{kwargs}") 198 | try: 199 | with open(file_path, 'x') as file: 200 | return "File created successfully." 201 | except FileExistsError: 202 | return "File already exists." 203 | 204 | def __count_lines(self, file_path, **kwargs): 205 | """Counts the number of lines in the specified file.""" 206 | lines = self.__read_file_lines(file_path) 207 | return f"Total lines: {len(lines)}" 208 | 209 | def __get_line(self, file_path, line_number, **kwargs): 210 | """Retrieves and returns a specific line from the specified file.""" 211 | lines = self.__read_file_lines(file_path) 212 | if line_number < 1 or line_number > len(lines): 213 | return "Line number is out of range." 214 | return lines[line_number - 1].strip() 215 | 216 | def __read_file_lines(self, file_path): 217 | """Reads all lines from the specified file and returns them as a list.""" 218 | with open(file_path, 'r') as file: 219 | return file.readlines() -------------------------------------------------------------------------------- /tools/folder_tool.py: -------------------------------------------------------------------------------- 1 | import logging 2 | logger = logging.getLogger(__name__) 3 | logger.debug(f"Entered {__file__}") 4 | from pydantic.v1 import BaseModel, Field 5 | from crewai_tools import BaseTool 6 | from typing import Type, Any 7 | import os 8 | from datetime import datetime 9 | from langchain.tools import tool 10 | 11 | 12 | 13 | 14 | class FolderToolSchema(BaseModel): 15 | """ 16 | Input schema for FolderTool, specifying the required parameters for listing files in a folder. 17 | """ 18 | folder_path: str = Field(..., description="folder path to list files from.") 19 | recursive: bool = Field(False, description="whether to list files recursively. Default is False.") 20 | 21 | class FolderTool(BaseTool): 22 | """ 23 | Tool to create and execute code using Open Interpreter. 24 | """ 25 | name: str = "FolderTool" 26 | description: str = "Tool to list files in a specified folder, with the option to list recursively. Takes in two parameters: 'folder_path' - the path to the folder, and 'recursive' - whether to list files recursively." 27 | args_schema: Type[BaseModel] = FolderToolSchema 28 | 29 | def __init__(self, **kwargs): 30 | super().__init__(**kwargs) 31 | self._generate_description() # Call to the inherited method to set the initial description 32 | 33 | def _run(self, **kwargs: Any) -> Any: 34 | """ 35 | Lists all files in a specified folder, with the option to list recursively. 36 | 37 | Parameters: 38 | - folder_path: Path to the folder. 39 | - recursive: Whether to list files recursively. 40 | 41 | Returns: 42 | A string indicating the number of files listed and the first 5 files, 43 | with a note on where to find the rest in the output file. 44 | """ 45 | folder_path = kwargs.get('folder_path') 46 | recursive = kwargs.get('recursive') 47 | # Generate the output file name with a timestamp 48 | output_file_name = f"find_{datetime.now().strftime('%Y%m%d%H%M%S')}.txt" 49 | # Assuming the output is to be saved in the current directory, modify as needed 50 | output_file_path = os.path.join(os.getcwd(), output_file_name) 51 | files_listed = [] 52 | 53 | # List files in the specified folder recursively or not, based on the recursive parameter 54 | if recursive: 55 | for root, dirs, files in os.walk(folder_path): 56 | for file in files: 57 | files_listed.append(os.path.join(root, file)) 58 | else: 59 | for item in os.listdir(folder_path): 60 | if os.path.isfile(os.path.join(folder_path, item)): 61 | files_listed.append(os.path.join(folder_path, item)) 62 | 63 | # Write the list of files to the output file 64 | with open(output_file_path, 'w') as output_file: 65 | for file_path in files_listed: 66 | output_file.write(file_path + '\n') 67 | 68 | # Prepare the output message 69 | if len(files_listed) > 5: 70 | first_5_files = "\n".join(files_listed[:5]) 71 | message = (f"{len(files_listed)} files were listed. Here are the first 5 lines:\n\n{first_5_files}\n" 72 | f"\n-- TOOL MESSAGE: End of part! --\n" 73 | f"The current output segment has concluded. Note: Additional content not displayed here.\n" 74 | f"ACTION REQUIRED: To continue reading the remaining lines, open the file: '{output_file_path}'\n") 75 | else: 76 | files = "\n".join(files_listed) 77 | message = f"{len(files_listed)} files were listed. Here are the files:\n{files}" 78 | 79 | return message 80 | -------------------------------------------------------------------------------- /tools/interpreter_tool.py: -------------------------------------------------------------------------------- 1 | import logging 2 | logger = logging.getLogger(__name__) 3 | logger.debug(f"Entered {__file__}") 4 | from pydantic.v1 import BaseModel, Field 5 | from crewai_tools import BaseTool 6 | from typing import Type, Any 7 | from langchain.tools import tool 8 | from interpreter import interpreter 9 | from langchain_openai import ChatOpenAI 10 | 11 | 12 | interpreter.auto_run = True 13 | #interpreter.offline = True # Disables online features like Open Procedures 14 | #interpreter.llm.model = "openai/x" # Tells OI to send messages in OpenAI's format 15 | #interpreter.llm.api_key = "fake_key" # LiteLLM, which we use to talk to LM Studio, requires this 16 | #interpreter.llm.api_base = "http://localhost:1234/v1" # Point this at any OpenAI compatible server 17 | interpreter.llm.context_window = 32768 #TODO remove hardcoding 18 | interpreter.llm.model = "openai/gpt-4-turbo-preview"#Todo remove hardcoding 19 | 20 | class CLIToolSchema(BaseModel): 21 | """ 22 | Input schema for CLIToolTool, specifying the required parameters for executing code. 23 | """ 24 | command: str = Field(..., description="command to be executed.") 25 | 26 | class CLITool(BaseTool): 27 | """ 28 | Tool to create and execute code using Open Interpreter. 29 | """ 30 | name: str = "Executor" 31 | description: str = "Tool to create and execute code using Open Interpreter. Takes in one parameter: 'command' - the command to be executed." 32 | args_schema: Type[BaseModel] = CLIToolSchema 33 | 34 | def __init__(self, **kwargs): 35 | super().__init__(**kwargs) 36 | self._generate_description() # Call to the inherited method to set the initial description 37 | 38 | def _run(self, **kwargs: Any) -> Any: 39 | command = kwargs.get('command') 40 | if not command: 41 | return "Error: No command provided for executing. Please provide a command as the 'command' parameter." 42 | 43 | interpreter.anonymized_telemetry = False 44 | result = interpreter.chat(command) 45 | return result 46 | -------------------------------------------------------------------------------- /utils/__init__.py: -------------------------------------------------------------------------------- 1 | from .sheets_loader import Sheets 2 | from .import_package_modules import import_package_modules 3 | from .safe_argment_parser import parse_arguments 4 | from .callable_registry import CallableRegistry 5 | from .helpers import load_env 6 | from .helpers import get_sheet_url_from_user 7 | from .tools_mapping import ToolsMapping 8 | from .tools_llm_config import ConfigurationManager 9 | from .cli_parser import get_parser 10 | from .agent_crew_llm import get_llm 11 | from .ollama_loader import OllamaLoader 12 | from .groq import TokenThrottledChatGroq -------------------------------------------------------------------------------- /utils/agent_crew_llm.py: -------------------------------------------------------------------------------- 1 | import logging 2 | logger = logging.getLogger(__name__) 3 | 4 | #from langchain_community.llms import Ollama 5 | from utils.ollama_loader import OllamaLoader 6 | import config.config as config 7 | from langchain_community.llms import HuggingFaceEndpoint 8 | from langchain_anthropic import ChatAnthropic 9 | from langchain_openai import ChatOpenAI, AzureOpenAI, AzureChatOpenAI 10 | #from langchain_groq import ChatGroq 11 | from utils.groq import TokenThrottledChatGroq 12 | from utils.helpers import load_env 13 | from ollama import pull, list 14 | import os 15 | from tqdm import tqdm 16 | 17 | def get_llm(model_name= None, temperature=0.7, num_ctx = None, provider = None, base_url = None, 18 | deployment=None, **kwargs): 19 | """ 20 | Retrieves an appropriate LLM based on specified parameters, including provider and model specifics. 21 | The function checks if the specific model or a base model already exists in Ollama and does not pull 22 | it if it does; otherwise, it attempts to pull the model. 23 | 24 | Parameters: 25 | - model_name (str): The name of the model to load, potentially including a version. 26 | - temperature (float): The temperature setting for the model. Affects the randomness of the responses. 27 | - num_ctx (int): Reserved for future use. Currently not used. 28 | - provider (str): The provider of the model ('openai', 'azure_openai', 'anthropic', etc.). 29 | - base_url (str): Base URL for the API requests, applicable for some providers like OpenAI and Azure. 30 | - deployment (str): Deployment specifics, primarily used for Azure. 31 | - progress (object): Progress tracking object, usually a UI element to indicate progress to the user. 32 | - llm_task (object): Task identifier for updating progress status. 33 | #TODO: - **kwargs: Additional keyword arguments that may be required by specific providers. Pass 34 | 35 | Returns: 36 | - An instance of the LLM based on the provider, configured with the given settings. 37 | 38 | Each provider uses different parameters: 39 | - 'anthropic': Uses 'model_name' and 'temperature'. 40 | - 'azure_openai': Uses 'deployment', 'base_url', 'temperature'. 41 | - 'openai': Uses 'model_name', 'temperature', 'base_url'. 42 | - 'huggingface': Uses 'model_name' and might use API token configurations internally. 43 | """ 44 | 45 | #load_env("../../ENV/.env", ["OPENAI_API_KEY","OPENAI_BASE_URL"]) 46 | 47 | #Anthropic. 48 | if provider.lower() == "anthropic": 49 | logger.info(f"Using Anthropic model '{model_name}' with temperature {temperature}.") 50 | try: 51 | return ChatAnthropic( 52 | model_name = model_name, #use model_name as endpoint 53 | api_key = os.environ.get("ANTHROPIC_API_KEY"), 54 | temperature = temperature, 55 | #stop = ["\nObservation"] 56 | ) 57 | except Exception as e: 58 | print(f"Hey, I've failed to configure Anthropic model '{model_name}'. Could you check if the API KEY is set? :\n{e}") 59 | return None 60 | 61 | #Azure OpenaI 62 | if provider.lower() == "azure_openai": 63 | logger.info(f"Trying {provider} model '{model_name}' with temperature {temperature}," \ 64 | f"deployment {deployment}, azure_andpoint {base_url}, AZURE_OPENAI_KEY, AZURE_OPENAI_VERSION.") 65 | try: 66 | return AzureChatOpenAI( 67 | azure_deployment = deployment, 68 | azure_endpoint = base_url, 69 | api_key = os.environ.get("AZURE_OPENAI_KEY"), 70 | api_version=os.environ.get("AZURE_OPENAI_VERSION"), 71 | temperature=temperature 72 | ) 73 | except Exception as e: 74 | print(f"Hey, I've failed to configure Azure OpenAI model '{model_name}'. Could you check if the API KEY is set? :\n{e}") 75 | return None 76 | 77 | #OpenAI 78 | if provider.lower() == "openai": 79 | logger.info(f"Trying {provider} model '{model_name}' with temperature {temperature}," 80 | f"base_url {base_url}, api_key via env.") 81 | try: 82 | os.environ['OPENAI_API_KEY'] = os.environ.get("SECRET_OPENAI_API_KEY") 83 | if base_url is None: 84 | base_url= "https://api.openai.com/v1" 85 | return ChatOpenAI( 86 | model = model_name, 87 | temperature = temperature, 88 | base_url = base_url, 89 | ) 90 | except Exception as e: 91 | print(f"Hey, I've failed to configure OpenAI model '{model_name}'. Could you check if the API KEY is set? :\n{e}") 92 | return None 93 | 94 | #OpenAI comatipble via /v1 protocol LM Studio, llamacpp, ollama, etc 95 | if provider.lower() == "openai_compatible": 96 | logger.info(f"Trying {provider} model '{model_name}' with temperature {temperature}, base_url {base_url}") 97 | try: 98 | return ChatOpenAI( 99 | model = model_name, 100 | temperature = temperature, 101 | base_url = base_url, 102 | openai_api_key = 'NA' #TODO suppoert for local llm API key's 103 | ) 104 | except Exception as e: 105 | print(f"Hey, I've failed to configure OpenAI model '{model_name}'. Could you check if the API KEY is set? :\n{e}") 106 | return None 107 | 108 | #Groq 109 | if provider.lower() == "groq": 110 | max_tokens = config.GroqConfig.max_tokens 111 | rate_limit = config.GroqConfig.get_rate_limit(model_name) 112 | logger.info(f"Trying {provider} model '{model_name}' with temperature {temperature}") 113 | try: 114 | return TokenThrottledChatGroq( #custom class to throttle tokens 115 | rate_limit = rate_limit, 116 | model = model_name, 117 | temperature = temperature, 118 | max_tokens = max_tokens, 119 | 120 | #base_url = base_url, 121 | ) 122 | except Exception as e: 123 | print(f"Hey, I've failed to configure Groq model '{model_name}'. Could you check if the API KEY is set?\n{e}") 124 | return None 125 | 126 | #huggingface 127 | if provider.lower() == "huggingface": 128 | logger.info(f"Trying {provider} repo_id '{model_name}' with hugingfacehub_api_token. via env") 129 | try: 130 | return HuggingFaceEndpoint( 131 | repo_id=model_name, 132 | huggingfacehub_api_token=os.environ.get("HUGGINGFACEHUB_API_TOKEN"), 133 | stop_sequences = config.HuggingFaceConfig.stop_sequences, 134 | #model_kwargs = {"max_length": 10000} #need to read documentation 135 | #max_new_tokens = 1000, #need to read documentation 136 | #max_length = 1000, #need to read documentation 137 | #task="text-generation", 138 | ) 139 | except Exception as e: 140 | print(f"Hey, I've failed to configure HuggingFace model '{model_name}'. Could you check if the API KEY is set? \n{e}") 141 | return None 142 | #Ollama 143 | if provider.lower() == "ollama": 144 | logger.info(f"Trying {provider} model '{model_name}' with num_ctx {num_ctx}.") 145 | try: 146 | return OllamaLoader.load( 147 | model_name = model_name, 148 | temperature = temperature, 149 | num_ctx = num_ctx, 150 | base_url = base_url, 151 | stop = config.OllamaConfig.stop_words #don't pass - crewai aleady does this itself. 152 | ) 153 | except Exception as e: 154 | print(f"Hey, I've failed to configure Ollama model '{model_name}':\n{e}") 155 | return None 156 | 157 | logger.error(f"Provider '{provider}' not recognized. Please use one of the supported providers: 'anthropic', 'azure_openai', 'openai', 'huggingface', 'ollama'.") 158 | return None 159 | 160 | 161 | 162 | 163 | 164 | -------------------------------------------------------------------------------- /utils/callable_registry.py: -------------------------------------------------------------------------------- 1 | 2 | import logging 3 | logger = logging.getLogger(__name__) 4 | from config.config import ToolsConfig 5 | import inspect 6 | 7 | callables_list = ToolsConfig.callables_list 8 | modules_list = ToolsConfig.modules_list 9 | 10 | class CallableRegistry: 11 | _instance = None # Singleton 12 | 13 | def __new__(cls): 14 | """Implement Singleton pattern. Only one instance of this class is created.""" 15 | if cls._instance is None: 16 | cls._instance = super(CallableRegistry, cls).__new__(cls) 17 | cls._instance.callable_dict = {} 18 | cls._instance.simple_name_dict = {} 19 | cls._instance.register_modules(modules_list) 20 | cls._instance.register_callables(callables_list) 21 | return cls._instance 22 | 23 | def register_modules(self, modules_list): 24 | """Registers all methods, functions, and classes from specified modules.""" 25 | for module, alias in modules_list: 26 | 27 | for name, obj in inspect.getmembers(module, predicate=lambda x: callable(x) or inspect.isclass(x)): 28 | qualified_name = f"{alias}.{name}" 29 | self._register_callable(name, qualified_name, obj) 30 | logger.info(f"Registered {qualified_name} as callable.") 31 | 32 | def register_callables(self, callables_list): #e.gg loat_tools is not callable without parameters 33 | """Registers specific callables provided in a list.""" 34 | for callable_item in callables_list: 35 | try: 36 | if callable(callable_item): 37 | callable_name = getattr(callable_item, '__name__', type(callable_item).__name__) 38 | callable_module = getattr(callable_item, '__module__', 'unknown_module') 39 | qualified_name = f"{callable_module}.{callable_name}" 40 | self._register_callable(callable_name, qualified_name, callable_item) 41 | except AttributeError as e: 42 | logging.error(f"Failed to register {callable_item}: {str(e)}") 43 | 44 | def _register_callable(self, name, qualified_name, callable_item): 45 | """Helper method to register a callable under both its full and simple names.""" 46 | self.callable_dict[qualified_name] = callable_item 47 | if name in self.simple_name_dict: 48 | # Handle name collisions: store multiple callables under the same simple name in a list 49 | if isinstance(self.simple_name_dict[name], list): 50 | self.simple_name_dict[name].append(callable_item) 51 | else: 52 | self.simple_name_dict[name] = [self.simple_name_dict[name], callable_item] 53 | else: 54 | self.simple_name_dict[name] = callable_item 55 | logger.info(f"Successfully registered callable: {qualified_name} as '{name}'.") 56 | 57 | def get_callable(self, name): 58 | """Retrieves a callable by its full name or simple name.""" 59 | if name in self.callable_dict: 60 | return self.callable_dict[name] 61 | elif name in self.simple_name_dict: 62 | result = self.simple_name_dict[name] 63 | if isinstance(result, list): 64 | logging.warning(f"Multiple callables found for '{name}'. Returning the first one.") 65 | return result[0] # Return the first callable from the list 66 | return result 67 | return None 68 | -------------------------------------------------------------------------------- /utils/cli_parser.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | logger = logging.getLogger(__name__) 4 | import argparse 5 | from config.config import AppConfig 6 | 7 | name, version = AppConfig.name, AppConfig.version 8 | 9 | 10 | def get_parser(): 11 | # ANSI escape codes for coloring 12 | green = '\033[92m' 13 | cyan = '\033[96m' 14 | red = '\033[91m' 15 | reset = '\033[0m' 16 | 17 | version_string = f"{green}{name}: {cyan}{version}{reset}" 18 | parser = argparse.ArgumentParser( 19 | description="This program processes data from a Google Sheet and sets logging preferences.", 20 | formatter_class=argparse.RawTextHelpFormatter) 21 | 22 | parser.add_argument('--sheet_url', 23 | help='The URL of the Google Sheet.\nExample: https://docs.google.com/spreadsheets/d/123abc/ \n') 24 | 25 | parser.add_argument("--loglevel", type=str, default="ERROR", help= 26 | """Set the log level to control logging output. \nChoices include: 27 | DEBUG - Low-level system information for debugging 28 | INFO - General system information 29 | WARNING - Information about minor problems 30 | ERROR - Information about major problems 31 | CRITICAL - Information about critical problems \nDefault: ERROR 32 | """) 33 | parser.add_argument("--env_path", type=str, default="../../ENV/.env") 34 | 35 | parser.add_argument("--version", action="version", version=version_string, 36 | help="Show program's version number and exit") 37 | 38 | # Parse the arguments to check the validity of loglevel 39 | args = parser.parse_args() 40 | if getattr(logging, args.loglevel.upper(), None) is None: 41 | parser.error(f"{red}Invalid log level: {args.loglevel}{reset}") 42 | 43 | return args 44 | -------------------------------------------------------------------------------- /utils/groq.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | import time 3 | import logging 4 | import asyncio 5 | from typing import (Any,AsyncIterator,Iterator,List,Optional,Union) 6 | from langchain_core.callbacks import (AsyncCallbackManagerForLLMRun,CallbackManagerForLLMRun,) 7 | from langchain_core.language_models.chat_models import (agenerate_from_stream,generate_from_stream) 8 | from langchain_core.messages import (AIMessageChunk, BaseMessage) 9 | from langchain_core.outputs import ChatGeneration, ChatGenerationChunk, ChatResult 10 | from langchain_core.pydantic_v1 import BaseModel 11 | from langchain_groq import ChatGroq 12 | from langchain_groq.chat_models import _convert_delta_to_message_chunk, _convert_dict_to_message 13 | from rich.console import Console 14 | import tiktoken 15 | #from tiktoken.core import Encoding 16 | #from tiktoken.model import encoding_for_model, encoding_name_for_model 17 | #from tiktoken.registry import get_encoding, list_encoding_names 18 | 19 | console = Console() 20 | logger = logging.getLogger(__name__) 21 | 22 | class Throttle: 23 | def __init__(self, rate_limit:int = None, average_token_length:int=5, model_name='gpt-4'): 24 | try: 25 | if rate_limit is None: 26 | logger.debug("Rate limit for Grog is not set. Not throtelling.") 27 | else: 28 | logger.debug("Rate limit for Grog is set. Setting up throtelling.") 29 | self.model_name = model_name 30 | self.rate_limit = rate_limit 31 | self.enc = tiktoken.encoding_for_model(model_name) 32 | self.average_token_length = average_token_length 33 | self.last_time_called = time.time() 34 | self.ratelimit_remaining_tokens = self.rate_limit 35 | 36 | logger.debug(f"/nThrottle Rate limit: {self.rate_limit}") 37 | logger.debug(f"Average token length : {self.average_token_length}") 38 | except Exception as e: 39 | logger.error(f"Failed to configure Throttle: {e}") 40 | 41 | 42 | def calculate_tokens(self, text=None): 43 | """Estimate number of tokens using the specific encoding model.""" 44 | if text is None: 45 | return 0 46 | try: 47 | encoded_text = self.enc.encode(text) 48 | token_count = len(encoded_text)* 1.1 #10$ safety margin 49 | logging.debug(f"Text: {text}") 50 | logging.debug(f"Token count: {token_count}") 51 | if token_count > self.rate_limit: 52 | exception = Exception(f"Token count exceeds rate limit: Token count:{token_count} Rate limit: {self.rate_limit} \n\ 53 | Please reduce the text length or increase the rate limit.") 54 | raise exception 55 | 56 | return token_count 57 | except Exception as e: 58 | logging.error(f"Failed to calculate tokens: {e}") 59 | return 0 60 | 61 | 62 | # def calculate_tokens(self, text = None): 63 | # """Estimate number of tokens based on text length.""" #Todo, use a better tokenization method 64 | # try: 65 | # if text is None: 66 | # return 0 67 | # else: 68 | # #logger.debug(text) 69 | # #logger.debug(f"Token length: {len(text) // self.average_token_length}") 70 | # return len(str(text)) // self.average_token_length 71 | # except Exception as e: 72 | # logger.error(f"Failed to calculate tokens: {e}") 73 | # return 0 74 | 75 | def _update_tokens(self): 76 | """Update tokens based on elapsed time.""" 77 | try: 78 | if self.rate_limit is None: #Don't throttle if rate limit is not set 79 | return 80 | 81 | now = time.time() 82 | elapsed = int(now - self.last_time_called) #Elapsed time in seconds 83 | tps = self.rate_limit // 60 #Tokens per second accumulated 84 | self.ratelimit_remaining_tokens += elapsed * tps #Accumulated tokens sine last call 85 | self.last_time_called = now #Update last call time 86 | logger.debug(f"ratelimit_remaining_tokens:{self.ratelimit_remaining_tokens}") 87 | except Exception as e: 88 | logger.error(f"Failed to update tokens: {e}") 89 | return 0 90 | 91 | def wait(self, tokens_needed: int): 92 | """Delay execution to respect the throttle limit.""" 93 | try: 94 | if self.rate_limit is None: #Don't throttle if rate limit is not set 95 | return 96 | 97 | self._update_tokens() 98 | 99 | if self.ratelimit_remaining_tokens >= tokens_needed: # If accumulated tokens exceed the needed 100 | self.ratelimit_remaining_tokens -= tokens_needed # Deduct the tokens needed 101 | return # No need to wait 102 | 103 | # Calculate sleep time to accumulate needed tokens 104 | tps = self.rate_limit // 60 # Tokens per second 105 | tokens_defecit = tokens_needed - self.ratelimit_remaining_tokens 106 | sleep_time = tokens_defecit // tps 107 | logger.debug(f"tokens_needed: {tokens_needed}") 108 | logger.debug(f"sleep_time: {sleep_time}") 109 | time.sleep(sleep_time) 110 | 111 | self._update_tokens() # Update tokens after sleep 112 | self.ratelimit_remaining_tokens -= tokens_needed 113 | except Exception as e: 114 | logger.error(f"Failed to wait: {e}") 115 | return 0 116 | 117 | async def await_(self, tokens_needed: int): 118 | """Asynchronous version of the wait.""" 119 | try: 120 | if self.rate_limit is None: #Don't throttle if rate limit is not set 121 | return 122 | 123 | self._update_tokens() 124 | 125 | if self.ratelimit_remaining_tokens >= tokens_needed: # If accumulated tokens exceed the needed 126 | self.ratelimit_remaining_tokens -= tokens_needed # Deduct the tokens needed 127 | return # No need to wait 128 | 129 | # Calculate sleep time to accumulate needed tokens 130 | tps = self.rate_limit // 60 # Tokens per second 131 | tokens_defecit = tokens_needed - self.ratelimit_remaining_tokens 132 | sleep_time = tokens_defecit // tps 133 | logger.debug(f"tokens_needed: {tokens_needed}") 134 | logger.debug(f"sleep_time: {sleep_time}") 135 | 136 | await asyncio.sleep(sleep_time) 137 | 138 | # Update tokens after sleep and adjust for token usage 139 | self._update_tokens() 140 | self.ratelimit_remaining_tokens -= tokens_needed 141 | except Exception as e: 142 | logger.error(f"Failed to await: {e}") 143 | return 0 144 | 145 | 146 | class TokenThrottledChatGroq(ChatGroq): 147 | def __init__(self, *args, rate_limit: Optional[int], **kwargs): 148 | super().__init__(*args, **kwargs) # Call the parent class constructor with additional arguments 149 | 150 | # SET UP THROTTLE 151 | self.rate_limit = rate_limit if rate_limit else None 152 | self.throttle = Throttle(rate_limit=self.rate_limit) 153 | 154 | # OVERRIDE pydantic_v1 BaseModel 155 | rate_limit: Optional[int] = None 156 | throttle: Throttle = Throttle(rate_limit=None) 157 | """Throttle settings for token generation.""" 158 | 159 | def _generate( 160 | self, 161 | messages: List[BaseMessage], 162 | stop: Optional[List[str]] = None, 163 | run_manager: Optional[CallbackManagerForLLMRun] = None, 164 | **kwargs: Any, 165 | ) -> ChatResult: 166 | 167 | #Throttle 168 | #for message in messages: 169 | # logger.debug("Debug: Type of message.content is", type(message.content)) 170 | # logger.debug("Debug: message.content is", message.content) 171 | 172 | input_text = "" 173 | for message in messages: 174 | input_text = input_text + message.content 175 | input_tokens = self.throttle.calculate_tokens(input_text) # Simplistic token count for input 176 | total_tokens = input_tokens + self.max_tokens #Assume we will get max_tokens 177 | self.throttle.wait(total_tokens) 178 | 179 | if self.streaming: 180 | stream_iter = self._stream(messages, stop=stop, run_manager=run_manager, **kwargs) 181 | return generate_from_stream(stream_iter) 182 | message_dicts, params = self._create_message_dicts(messages, stop) 183 | params = {**params, **kwargs} 184 | response = self.client.create(messages=message_dicts, **params) 185 | #logger.debug("Response: ", response) 186 | 187 | 188 | return self._create_chat_result(response) 189 | 190 | async def _agenerate( 191 | self, 192 | messages: List[BaseMessage], 193 | stop: Optional[List[str]] = None, 194 | run_manager: Optional[AsyncCallbackManagerForLLMRun] = None, 195 | **kwargs: Any, 196 | ) -> ChatResult: 197 | #Throttle 198 | #for message in messages: 199 | # logger.debug("Debug: Type of message.content is", type(message.content)) 200 | # logger.debug("Debug: message.content is", message.content) 201 | 202 | input_text = "" 203 | for message in messages: 204 | input_text = input_text + message.content 205 | input_tokens = self.throttle.calculate_tokens(input_text) # Simplistic token count for input 206 | total_tokens = input_tokens + self.max_tokens 207 | self.throttle.await_(total_tokens) 208 | 209 | if self.streaming: 210 | stream_iter = self._astream(messages, stop=stop, run_manager=run_manager, **kwargs) 211 | return await agenerate_from_stream(stream_iter) 212 | 213 | message_dicts, params = self._create_message_dicts(messages, stop) 214 | params = { 215 | **params, 216 | **kwargs, 217 | } 218 | response = await self.async_client.create(messages=message_dicts, **params) 219 | #logger.debug("Response: ", response) 220 | #logger.debug("Response type: ", type(response)) 221 | return self._create_chat_result(response) 222 | 223 | def _stream( 224 | self, 225 | messages: List[BaseMessage], 226 | stop: Optional[List[str]] = None, 227 | run_manager: Optional[CallbackManagerForLLMRun] = None, 228 | **kwargs: Any, 229 | ) -> Iterator[ChatGenerationChunk]: 230 | #Throttle 231 | #for message in messages: 232 | # logger.debug("Debug: Type of message.content is", type(message.content)) 233 | # logger.debug("Debug: message.content is", message.content) 234 | 235 | input_text = "" 236 | for message in messages: 237 | input_text = input_text + message.content 238 | input_tokens = self.throttle.calculate_tokens(input_text) # Simplistic token count for input 239 | total_tokens = input_tokens + self.max_tokens 240 | self.throttle.wait(total_tokens) 241 | 242 | message_dicts, params = self._create_message_dicts(messages, stop) 243 | 244 | # groq api does not support streaming with tools yet 245 | if "tools" in kwargs: 246 | response = self.client.create( 247 | messages=message_dicts, **{**params, **kwargs} 248 | ) 249 | chat_result = self._create_chat_result(response) 250 | generation = chat_result.generations[0] 251 | message = generation.message 252 | tool_call_chunks = [ 253 | { 254 | "name": rtc["function"].get("name"), 255 | "args": rtc["function"].get("arguments"), 256 | "id": rtc.get("id"), 257 | "index": rtc.get("index"), 258 | } 259 | for rtc in message.additional_kwargs["tool_calls"] 260 | ] 261 | chunk_ = ChatGenerationChunk( 262 | message=AIMessageChunk( 263 | content=message.content, 264 | additional_kwargs=message.additional_kwargs, 265 | tool_call_chunks=tool_call_chunks, 266 | ), 267 | generation_info=generation.generation_info, 268 | ) 269 | if run_manager: 270 | geninfo = chunk_.generation_info or {} 271 | run_manager.on_llm_new_token( 272 | chunk_.text, 273 | chunk=chunk_, 274 | logprobs=geninfo.get("logprobs"), 275 | ) 276 | yield chunk_ 277 | return 278 | 279 | params = {**params, **kwargs, "stream": True} 280 | 281 | default_chunk_class = AIMessageChunk 282 | for chunk in self.client.create(messages=message_dicts, **params): 283 | if not isinstance(chunk, dict): 284 | chunk = chunk.dict() 285 | if len(chunk["choices"]) == 0: 286 | continue 287 | choice = chunk["choices"][0] 288 | chunk = _convert_delta_to_message_chunk(choice["delta"], AIMessageChunk) 289 | generation_info = {} 290 | if finish_reason := choice.get("finish_reason"): 291 | generation_info["finish_reason"] = finish_reason 292 | logprobs = choice.get("logprobs") 293 | if logprobs: 294 | generation_info["logprobs"] = logprobs 295 | default_chunk_class = chunk.__class__ 296 | chunk = ChatGenerationChunk( 297 | message=chunk, generation_info=generation_info or None 298 | ) 299 | 300 | if run_manager: 301 | run_manager.on_llm_new_token(chunk.text, chunk=chunk, logprobs=logprobs) 302 | yield chunk 303 | 304 | async def _astream( 305 | self, 306 | messages: List[BaseMessage], 307 | stop: Optional[List[str]] = None, 308 | run_manager: Optional[AsyncCallbackManagerForLLMRun] = None, 309 | **kwargs: Any, 310 | ) -> AsyncIterator[ChatGenerationChunk]: 311 | #Throttle 312 | #for message in messages: 313 | # logger.debug("Debug: Type of message.content is", type(message.content)) 314 | # logger.debug("Debug: message.content is", message.content) 315 | 316 | input_text = "" 317 | for message in messages: 318 | input_text = input_text + message.content 319 | input_tokens = self.throttle.calculate_tokens(input_text) # Simplistic token count for input 320 | total_tokens = input_tokens + self.max_tokens 321 | self.throttle.wait(total_tokens) 322 | 323 | 324 | message_dicts, params = self._create_message_dicts(messages, stop) 325 | 326 | # groq api does not support streaming with tools yet 327 | if "tools" in kwargs: 328 | response = await self.async_client.create( 329 | messages=message_dicts, **{**params, **kwargs} 330 | ) 331 | chat_result = self._create_chat_result(response) 332 | generation = chat_result.generations[0] 333 | message = generation.message 334 | tool_call_chunks = [ 335 | { 336 | "name": rtc["function"].get("name"), 337 | "args": rtc["function"].get("arguments"), 338 | "id": rtc.get("id"), 339 | "index": rtc.get("index"), 340 | } 341 | for rtc in message.additional_kwargs["tool_calls"] 342 | ] 343 | chunk_ = ChatGenerationChunk( 344 | message=AIMessageChunk( 345 | content=message.content, 346 | additional_kwargs=message.additional_kwargs, 347 | tool_call_chunks=tool_call_chunks, 348 | ), 349 | generation_info=generation.generation_info, 350 | ) 351 | if run_manager: 352 | geninfo = chunk_.generation_info or {} 353 | await run_manager.on_llm_new_token( 354 | chunk_.text, 355 | chunk=chunk_, 356 | logprobs=geninfo.get("logprobs"), 357 | ) 358 | yield chunk_ 359 | return 360 | 361 | params = {**params, **kwargs, "stream": True} 362 | 363 | default_chunk_class = AIMessageChunk 364 | async for chunk in await self.async_client.create( 365 | messages=message_dicts, **params 366 | ): 367 | if not isinstance(chunk, dict): 368 | chunk = chunk.dict() 369 | if len(chunk["choices"]) == 0: 370 | continue 371 | choice = chunk["choices"][0] 372 | chunk = _convert_delta_to_message_chunk( 373 | choice["delta"], default_chunk_class 374 | ) 375 | generation_info = {} 376 | if finish_reason := choice.get("finish_reason"): 377 | generation_info["finish_reason"] = finish_reason 378 | logprobs = choice.get("logprobs") 379 | if logprobs: 380 | generation_info["logprobs"] = logprobs 381 | default_chunk_class = chunk.__class__ 382 | chunk = ChatGenerationChunk( 383 | message=chunk, generation_info=generation_info or None 384 | ) 385 | 386 | if run_manager: 387 | await run_manager.on_llm_new_token( 388 | token=chunk.text, chunk=chunk, logprobs=logprobs 389 | ) 390 | yield chunk 391 | 392 | def _create_chat_result(self, response: Union[dict, BaseModel]) -> ChatResult: 393 | generations = [] 394 | if not isinstance(response, dict): 395 | response = response.dict() 396 | for res in response["choices"]: 397 | message = _convert_dict_to_message(res["message"]) 398 | generation_info = dict(finish_reason=res.get("finish_reason")) 399 | if "logprobs" in res: 400 | generation_info["logprobs"] = res["logprobs"] 401 | gen = ChatGeneration( 402 | message=message, 403 | generation_info=generation_info, 404 | ) 405 | generations.append(gen) 406 | token_usage = response.get("usage", {}) 407 | llm_output = { 408 | "token_usage": token_usage, 409 | "model_name": self.model_name, 410 | "system_fingerprint": response.get("system_fingerprint", ""), 411 | } 412 | #logger.debug("token_usage: ", token_usage) 413 | self.throttle.ratelimit_remaining_tokens += self.max_tokens - token_usage['total_tokens'] #Trottle: release unused tokens TODO: call function 414 | self.throttle._update_tokens() 415 | return ChatResult(generations=generations, llm_output=llm_output) 416 | -------------------------------------------------------------------------------- /utils/helpers.py: -------------------------------------------------------------------------------- 1 | import logging 2 | logger = logging.getLogger(__name__) 3 | logger.debug(f"Entered {__file__}") 4 | from rich.markdown import Markdown 5 | from rich.console import Console 6 | from rich.table import Table 7 | import os 8 | from dotenv import load_dotenv 9 | 10 | import numpy as np 11 | from config.config import AppConfig 12 | template_sheet_url = AppConfig.template_sheet_url 13 | 14 | # ASCII art and greetings print functions 15 | def greetings_print(): 16 | console = Console() 17 | console = Console(force_terminal=True, force_interactive=True) 18 | # ASCII art 19 | ascii_art = """ 20 | [green] ██████ ██████ ███████ ██ ██ █████ ██ ██████ ██ ██ ██ 21 | [yellow]██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ 22 | [orange]██ ██████ █████ ██ █ ██ ███████ ██ ██ ███ ██ ██ ██ 23 | [red]██ ██ ██ ██ ██ ███ ██ ██ ██ ██ ██ ██ ██ ██ ██ 24 | [magenta] ██████ ██ ██ ███████ ███ ███ ██ ██ ██ ██████ ██████ ██ 25 | [blue]███████ ███████ ███████ ███████ ███████ ███████ ████████ ██████ ██████ ███████ 26 | """ 27 | 28 | console.print(ascii_art) 29 | # Create a markdown string for the greeting message 30 | greeting_message = f""" 31 | # Howdy, ready to use this awsome app? 32 | 33 | To get you started, copy this sheet template and create your agents and tasks. I'm waiting for you inside the sheet! :D 34 | [{template_sheet_url}]({template_sheet_url}) 35 | """ 36 | # Print the greeting using Rich's Markdown support for nice formatting 37 | console.print(Markdown(greeting_message)) 38 | console.print("\n") 39 | 40 | 41 | def after_read_sheet_print(agents_df, tasks_df): 42 | console = Console() 43 | terminal_width = console.width # Get the current width of the terminal 44 | terminal_width = max(terminal_width, 120) 45 | 46 | # Create a table for agents 47 | agents_table = Table(show_header=True, header_style="bold magenta") 48 | agent_role_width = max(int(terminal_width * 0.1), 10) # 20% of terminal width, at least 20 characters 49 | goal_width = max(int(terminal_width * 0.3), 30) # 40% of terminal width, at least 40 characters 50 | backstory_width = max(int(terminal_width * 0.6), 60) # 40% of terminal width, at least 40 characte 51 | agents_table.add_column("Agent Role", style="dim", width=agent_role_width) 52 | agents_table.add_column("Goal", width=goal_width) 53 | agents_table.add_column("Backstory", width=backstory_width) 54 | 55 | for index, row in agents_df.iterrows(): 56 | agents_table.add_row() 57 | agents_table.add_row(row['Agent Role'], row['Goal'], row['Backstory']) 58 | 59 | 60 | # Tasks Table 61 | task_name_width = max(int(terminal_width * 0.1),10) # 20% of terminal width, at least 20 characters 62 | agent_width = max(int(terminal_width * 0.2), 20) # 20% of terminal width, at least 20 characters 63 | instructions_width = max(int(terminal_width * 0.7), 70) # 60% of terminal width, at least 60 characters 64 | tasks_table = Table(show_header=True, header_style="bold magenta") 65 | tasks_table.add_column("Task Name", style="dim", width=task_name_width) 66 | tasks_table.add_column("Agent", width=agent_width) 67 | tasks_table.add_column("Instructions", width=instructions_width) 68 | 69 | for index, row in tasks_df.iterrows(): 70 | tasks_table.add_row() 71 | tasks_table.add_row(row['Task Name'], row['Agent'], row['Instructions']) 72 | 73 | 74 | console.print("\nI found these agents and tasks in the google sheet. Let's get your crew runing:") 75 | # Display the tables 76 | console.print(agents_table) 77 | console.print(tasks_table) 78 | 79 | 80 | 81 | # Function to load environment variables 82 | def load_env(env_path, expected_vars=None): 83 | """ 84 | Load environment variables from a .env file and verify expected variables with stylized print output. 85 | 86 | :param env_path: Path to the .env file, can be relative or absolute. 87 | :param expected_vars: A list of environment variable names that are expected to be set. 88 | """ 89 | # Convert to absolute path if necessary 90 | if not os.path.isabs(env_path): 91 | env_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), env_path) 92 | 93 | loaded = load_dotenv(env_path) 94 | if not loaded: 95 | print(f"I failed to load the .env file from '{env_path}'. I'm so sorry, the environmen variables may not be set.") 96 | return 97 | 98 | if expected_vars: 99 | missing_vars = [var for var in expected_vars if not os.getenv(var)] 100 | if missing_vars: 101 | logger.info(f"I was expecting these environemnt variables,: {', '.join(missing_vars)}, but maybe it will be ok...\n") 102 | 103 | 104 | 105 | import re 106 | from urllib.parse import urlparse 107 | 108 | def is_valid_google_sheets_url(url): 109 | if not url: # Early return if URL is None or empty 110 | return False 111 | 112 | try: 113 | parsed_url = urlparse(url) 114 | if parsed_url.netloc != 'docs.google.com' or not parsed_url.path.startswith('/spreadsheets/d/'): 115 | print("I'm confused, it says its' not a Google Sheet URL *confused face* :/") 116 | return False 117 | 118 | # Updated regex for improved accuracy 119 | match = re.match(r'^/spreadsheets/d/([a-zA-Z0-9-_]+)', parsed_url.path) 120 | if not match: 121 | print("You fonnd the fist easter egg. This looks like a partially correct Google Sheet URL. Let's try again? : ") 122 | return False 123 | 124 | return True 125 | except Exception as e: 126 | print(f"The computer says: Error parsing URL... {e}") 127 | return False 128 | 129 | def get_sheet_url_from_user(): 130 | sheet_url = input("Let's copy paste the Google Sheet url and get started: ") 131 | while not is_valid_google_sheets_url(sheet_url): 132 | sheet_url = input("Could you doule check the URL? This silly box is saying it's invalid :/ : ") 133 | return sheet_url 134 | 135 | 136 | 137 | # # Helper function to convert strings to boolean 138 | # def str_to_bool(value_str): 139 | # if isinstance(value_str, (bool, np.bool_)): 140 | # return value_str 141 | # else: 142 | # return value_str.lower() in ['true', '1', 't', 'y', 'yes'] -------------------------------------------------------------------------------- /utils/import_package_modules.py: -------------------------------------------------------------------------------- 1 | import logging 2 | logger = logging.getLogger(__name__) 3 | logger.debug(f"Entered {__file__}") 4 | import pkgutil 5 | import importlib 6 | 7 | logger = logging.getLogger(__name__) 8 | logger.debug(f"Entered the module {__file__}") 9 | #from utils.helpers import load_env 10 | #load_env("../../ENV/.env", ["OPENAI_API_KEY","OPENAI_BASE_URL"]) 11 | 12 | def import_package_modules(package, modules_list, integration_dict, recursive=False): 13 | """ 14 | Dynamically imports all submodules from the specified package, appends them to modules_list, 15 | and populates an integration dictionary with all public members of the modules. 16 | 17 | Args: 18 | package (module): The package from which to import all submodules. 19 | modules_list (list): List to which module references will be appended. 20 | integration_dict (dict): Dictionary to which public module members are added. 21 | recursive (bool): If True, imports submodules recursively. 22 | 23 | Returns: 24 | None: Modifies modules_list and integration_dict in-place. 25 | """ 26 | package_path = package.__path__ # This gets the package path iterable 27 | package_name = package.__name__ # Get the full package name 28 | 29 | # Iterate through the package modules 30 | for loader, name, ispkg in pkgutil.walk_packages(package_path, prefix=package_name + '.'): 31 | try: 32 | module = importlib.import_module(name) 33 | modules_list.append((module, name)) 34 | # Integrate public members, irrespective of whether the module is a package 35 | for attr_name in dir(module): 36 | if not attr_name.startswith('_'): # Include only public members 37 | obj = getattr(module, attr_name) 38 | integration_dict[attr_name] = obj 39 | 40 | logger.info(f"Imported and added module: {name}") 41 | # Recursively import submodules if it is a package and recursive is True 42 | if recursive and ispkg: 43 | import_package_modules(module, modules_list, integration_dict, recursive=True) 44 | except ImportError as e: 45 | logger.warning(f"Failed to import {name}: {e}") 46 | 47 | -------------------------------------------------------------------------------- /utils/ollama_loader.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import socket 3 | from langchain_community.llms.ollama import Ollama 4 | from rich.progress import Progress 5 | from ollama import list, pull, Client 6 | logger = logging.getLogger(__name__) 7 | 8 | def running_in_docker(): 9 | try: 10 | # This will try to resolve the special Docker DNS name for the host. 11 | host_ip = socket.gethostbyname('host.docker.internal') 12 | #print(f"Hey, it looks like I'm running inside a docker container.") 13 | #print(f"There was no base_url set for this model, so I'll assume it's {host_ip}:11434.") 14 | return True if host_ip else False 15 | except socket.gaierror: 16 | # The name is not known, which likely means not running inside Docker 17 | return False 18 | 19 | class OllamaLoader: 20 | 21 | 22 | def handle_progress_updates(progress_update, progress, llm_task): 23 | """ 24 | Handles progress updates during model download and initialization. 25 | """ 26 | if 'total' in progress_update: 27 | progress.update(llm_task, total=progress_update['total']) 28 | if 'completed' in progress_update: 29 | current_completed = progress.tasks[llm_task].completed 30 | new_advance = progress_update['completed'] - current_completed 31 | progress.advance(llm_task, advance=new_advance) 32 | if 'status' in progress_update: 33 | progress.update(llm_task, advance=0, description=f"[cyan]LLM: {progress_update['status']}") 34 | 35 | def load(model_name=None, temperature=0.8, num_ctx=None, base_url=None, **kwargs): 36 | """ 37 | Loads the specified model from Ollama, or pulls it if it does not exist. 38 | """ 39 | 40 | if running_in_docker(): 41 | if base_url is None: 42 | base_url = 'http://host.docker.internal:11434' #TODO: Move to config 43 | ollama_client = Client(host=base_url) 44 | else: 45 | ollama_client = None 46 | 47 | 48 | parts = model_name.split(':') 49 | if len(parts) < 2 : 50 | print (f"Ollama models usually have a version, like {model_name}:instruct, or {model_name}:latest. That's ok, I'll take a guess and use the latest version.") 51 | 52 | 53 | model_list = list()['models'] if ollama_client is None else ollama_client.list()['models'] 54 | 55 | for model in model_list: 56 | if model['name'] == model_name: 57 | logger.info(f"Model '{model_name}' found in Ollama, loading directly.") 58 | 59 | if base_url is not None: 60 | return Ollama(model=model_name, temperature=temperature, num_ctx=num_ctx, base_url=base_url) 61 | else: 62 | return Ollama(model=model_name, temperature=temperature, num_ctx=num_ctx) 63 | 64 | logger.info(f"No local matching model found for '{model_name}' in Ollama.") 65 | print(f"I'm trying to download '{model_name}' from Ollama... This may take a while. Why not grab a cup of coffee...") 66 | 67 | progress = Progress(expand=True, transient=True) 68 | with progress: 69 | llm_task = progress.add_task(f"Downloading '{model_name}'", total=1000) 70 | if ollama_client is None: 71 | for response in pull(model=model_name, stream=True): 72 | OllamaLoader.handle_progress_updates(response, progress, llm_task) 73 | else: 74 | for response in ollama_client.pull(model=model_name, stream=True): 75 | OllamaLoader.handle_progress_updates(response, progress, llm_task) 76 | 77 | logger.info(f"Model '{model_name}' successfully pulled") 78 | logger.info(f"Attempting to load model '{model_name}'...") 79 | print(f"Model '{model_name}' successfully pulled. Now I'm trying to load it...") 80 | if base_url is not None: 81 | return Ollama(model=model_name, temperature=temperature, num_ctx=num_ctx, base_url=base_url) 82 | else: 83 | return Ollama(model=model_name, temperature=temperature, num_ctx=num_ctx) 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | # def load_model(self): 92 | # """Selects the appropriate method to load the model based on the model name. 93 | # If the model is not found in Ollama, it attempts to pull it with streaming enabled. 94 | # Finds the best matching model from the list of existing models based on a structured naming convention. 95 | # Models can have different suffixes and versions, and this method tries to find the most appropriate match 96 | # based on predefined rules. 97 | # #exact match1 _crewai_: == _crewai_num_ctx: 98 | # #exact match2 _crewai_ == _crewai_num_ctx:latest 99 | # -->just lod the model 100 | # #crew match1 _crew: == _crew: 101 | # #crew match2 _crew == _crew:latest 102 | # #model match : == : 103 | # #model match == :latest 104 | # -->meed to patch modelfile first 105 | # """ 106 | # if self.model_name is None: 107 | # logger.error("Model name is None, cannot load model.") 108 | # return None 109 | # version = 'latest' if ':' not in self.model_name else self.model_name.split(':')[1] 110 | # model_base = self.model_name.split(':')[0] 111 | # model_name_crewai_num_ctx = f"{model_base}_crewai_{self.num_ctx}" 112 | # model_name_crewai = f"{model_base}_crewai" 113 | # model_name_base = model_base 114 | # model_base_version = f"{model_base}:{version}" 115 | 116 | # ollama_models = list()['models'] 117 | # match = None 118 | # for o_model in ollama_models: 119 | # o_model_name = o_model['name'] 120 | # # Check for exact matches including num_ctx and version 121 | # if o_model_name == f"{model_name_crewai_num_ctx}:{version}": 122 | # match = "crewai_num_ctx" 123 | # # Check for matches with 'crewai' suffix and possibly handling versions 124 | # elif o_model_name == f"{model_name_crewai}:{version}": 125 | # match = "crewai" 126 | # # Check for base model matches with versions 127 | # elif o_model_name == f"{model_name_base}:{version}": 128 | # match = "base" 129 | # return self.ollama_patch_and_load(o_model=model_base_version, num_ctx=self.num_ctx, match=match) 130 | 131 | # logger.info(f"No local matching model found for '{self.model_name}' in Ollama.") 132 | # logger.info(f"Attempting to pull {self.model_name} with streaming enabled.") 133 | # return self.ollama_pull_and_load(model_name = self.model_name) 134 | 135 | # def ollama_patch_and_load(self, o_model, num_ctx, match=None): 136 | # """ 137 | # Patches the model file with specific configurations such as num_ctx and stop words, then loads it. 138 | # """ 139 | # patch_stop_words = OllamaConfig.patch_stop_words 140 | # patch_num_ctx = OllamaConfig.patch_num_ctx 141 | # stop_words = OllamaConfig.stop_words 142 | 143 | # #No patching required, skip all, load the model directly 144 | # if (patch_stop_words == False and (patch_num_ctx == False or num_ctx is None)) or \ 145 | # (patch_num_ctx == False and stop_words == []): 146 | # logger.info(f"No patching required for model '{o_model}', loading directly...") 147 | # return Ollama(model=o_model) 148 | # #<----- 149 | 150 | # #get the base model modelfile num_ctx and stop words 151 | # o_model_details = show(o_model) 152 | # o_modelfile = o_model_details['modelfile'] 153 | # c_modelfile_nctx_list = re.findall(r'PARAMETER num_ctx (\d+)', c_modelfile) 154 | # c_modelfile_stop_list = re.findall(r'PARAMETER stop (.*)', c_modelfile) 155 | 156 | # #get the crewai model modelfile num_ctx and stop words for comparison 157 | # version = 'latest' if ':' not in o_model else o_model.split(':')[1] 158 | # if match == "crewai_num_ctx": 159 | # crewai_model_name = o_model.split(':')[0] + "_crewai_" + str(num_ctx) + "_:" + version 160 | # c_model_details = show(crewai_model_name) 161 | # c_modelfile = c_model_details['modelfile'] 162 | # elif match == "crewai": 163 | # crewai_model_name = o_model.split(':')[0] + "_crewai" + version 164 | # c_model_details = show(crewai_model_name) 165 | # c_modelfile = c_model_details['modelfile'] 166 | # else: 167 | # c_modelfile = o_modelfile 168 | # c_modelfile_nctx = c_modelfile_nctx_list[0] if c_modelfile_nctx_list else None #there is only one num_ctx 169 | # combined_stop_words = list(set(stop_words + c_modelfile_stop_list)) 170 | 171 | # #num_ctx and original stop words and config defined stop word match 172 | # if c_modelfile_nctx == num_ctx and c_modelfile_stop_list == combined_stop_words: 173 | # logger.info(f"Model '{crewai_model_name}' found with correct num_ctx and stop words, loading directly.") 174 | # return Ollama(model=crewai_model_name) 175 | # #<----- 176 | 177 | # else: #patch modelfile 178 | # logger.info(f"Model '{crewai_model_name}' found with unmatching num_ctx or stop words, patching model.") 179 | # logger.info(f"Patching model: {o_model} with num_ctx = {num_ctx} and stop words: {stop_words}") 180 | 181 | # if patch_stop_words and stop_words != []: 182 | # for stop_word in stop_words: 183 | # if stop_word not in modelfile: 184 | # modelfile += f"\nPARAMETER stop {stop_word}" 185 | 186 | # if patch_num_ctx and num_ctx: 187 | # modelfile += f"\nPARAMETER num_ctx {num_ctx}" 188 | 189 | # #calculate name of the patched model 190 | # if num_ctx: 191 | # crewai_model_name = f"{o_model.split(':')[0]}_crewai_{num_ctx}:{version}" 192 | # else: 193 | # crewai_model_name = f"{o_model.split(':')[0]}_crewai:{version}" 194 | 195 | # #create the patched model 196 | # logger.info(f"Creating new model '{crewai_model_name}' with updated stop parameters."\ 197 | # f"num_ctx : {num_ctx}, stop_words: {stop_words}") 198 | 199 | # for response in create(model=crewai_model_name, modelfile=modelfile, stream=True): 200 | # self.handle_progress_updates(response, self.progress, self.llm_task) 201 | # return Ollama(model=crewai_model_name) 202 | -------------------------------------------------------------------------------- /utils/safe_argment_parser.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | logger = logging.getLogger(__name__) 4 | from RestrictedPython.PrintCollector import PrintCollector 5 | from RestrictedPython.Guards import safe_builtins 6 | from RestrictedPython.Eval import default_guarded_getitem 7 | from RestrictedPython import compile_restricted 8 | from config.config import ToolsConfig 9 | import pandas as pd 10 | 11 | integration_dict = ToolsConfig.integration_dict 12 | 13 | 14 | def get_safe_execution_environment(integration_dict): 15 | """ 16 | Prepare a safe execution environment for RestrictedPython using an integration dictionary 17 | populated with dynamically imported module members. 18 | 19 | Args: 20 | integration_dict (dict): Dictionary containing public members from dynamically imported modules. 21 | 22 | Returns: 23 | tuple: A tuple containing dictionaries for globals and locals for the execution environment. 24 | """ 25 | safe_globals = safe_builtins.copy() 26 | # Update safe_globals with the contents of integration_dict 27 | safe_globals.update(integration_dict) 28 | 29 | safe_locals = { 30 | '_print_': PrintCollector(), 31 | '_getitem_': default_guarded_getitem 32 | } 33 | return safe_globals, safe_locals 34 | 35 | 36 | def parse_arguments(arg_str): 37 | max_length = 1000 38 | if pd.isna(arg_str): 39 | return [], {} 40 | if len(arg_str) > max_length: 41 | logger.error(f"Argument string exceeds maximum length of {max_length} characters.") 42 | raise ValueError(f"Input too long. Maximum allowed length is {max_length} characters.") 43 | 44 | args, kwargs = [], {} 45 | globals_dict, locals_dict = get_safe_execution_environment(integration_dict) 46 | 47 | for pair in arg_str.split(','): 48 | pair = pair.strip() 49 | if not pair: 50 | continue 51 | 52 | key_value = pair.split('=') 53 | if len(key_value) == 2: 54 | key, value = map(str.strip, key_value) 55 | if not key.isidentifier(): 56 | logger.error(f"Invalid keyword '{key}'. Must be a valid identifier.") 57 | raise ValueError(f"Invalid keyword '{key}'. Must be a valid identifier.") 58 | byte_code = compile_restricted(value, '', 'eval') 59 | try: 60 | kwargs[key] = eval(byte_code, globals_dict, locals_dict) 61 | except Exception as e: 62 | logger.error(f"Error evaluating expression '{value}': {e}") 63 | raise ValueError(f"Error evaluating expression: {e}") 64 | elif len(key_value) == 1: 65 | value = key_value[0].strip() 66 | byte_code = compile_restricted(value, '', 'eval') 67 | try: 68 | args.append(eval(byte_code, globals_dict, locals_dict)) 69 | except Exception as e: 70 | logger.error(f"Error evaluating expression '{value}': {e}") 71 | raise ValueError(f"Error evaluating expression: {e}") 72 | else: 73 | logger.error("Malformed argument. Use 'key=value' for kwargs.") 74 | raise ValueError("Malformed argument. Use 'key=value' for kwargs.") 75 | 76 | return args, kwargs 77 | -------------------------------------------------------------------------------- /utils/sheets_loader.py: -------------------------------------------------------------------------------- 1 | import logging 2 | logger = logging.getLogger(__name__) 3 | from config.config import AppConfig 4 | from urllib.error import URLError 5 | from textwrap import dedent 6 | from utils.helpers import get_sheet_url_from_user 7 | import pandas as pd 8 | import sys 9 | 10 | template_sheet_url = AppConfig.template_sheet_url 11 | class Sheets: 12 | @staticmethod 13 | def read_google_sheet(sheet_url): 14 | # Extract the base URL from the provided Google Sheet URL 15 | base_url = sheet_url.split('/edit')[0] 16 | dataframes = [] 17 | # Define the worksheets and their respective columns to be read 18 | worksheets = { 19 | 'Agents': ['Agent Role', 'Goal', 'Backstory', 'Tools', 'Allow delegation', 'Verbose', 'Memory', 'Max_iter','Model Name', 'Temperature', 'Function Calling Model'], 20 | 'Tasks' : ['Task Name', 'Agent', 'Instructions', 'Expected Output'], 21 | 'Crew' : ['Team Name', 'Assignment','Verbose', 'Process', 'Memory', 'Embedding model', 'Manager LLM', 't', 'num_ctx'], 22 | 'Models': ['Model', 'Context size (local only)', 'Provider', 'base_url','Deployment'], 23 | 'Tools' : ['Tool', 'On', 'Class', 'Args', 'Model', 'Embedding Model'] 24 | } 25 | 26 | for worksheet, columns in worksheets.items(): 27 | url = f'{base_url}/gviz/tq?tqx=out:csv&sheet={worksheet}' 28 | # Read the worksheet into a DataFrame, selecting only the specified columns 29 | try: 30 | data = pd.read_csv(url, usecols=columns) 31 | if worksheet == 'Agents': # sanitize the data 32 | data.dropna(subset=['Agent Role'], inplace=True) 33 | 34 | for col in ['Agent Role', 'Goal','Backstory', 'Tools', 'Model Name', 'Function Calling Model']: 35 | data[col] = data[col].astype(str).apply(dedent).replace('None', None) 36 | 37 | for col in ['Tools']: 38 | data[col] = data[col].replace('\n','') 39 | for col in ['Allow delegation', 'Verbose', 'Memory']: 40 | data[col] = data[col].astype(bool) 41 | 42 | data['Temperature'] = data['Temperature'].astype(float) 43 | data['Max_iter'] = data['Max_iter'].astype(int) 44 | 45 | if worksheet == 'Models': 46 | data['Context size (local only)'] = data['Context size (local only)'].replace(0, None) 47 | data['base_url'] = data['base_url'].replace("None", None) 48 | data['Deployment'] = data['Deployment'].replace("None", None) 49 | if worksheet == 'Tasks': 50 | #check if all columns are present are string. If not, print error and exit 51 | for col in columns: 52 | #convert all columns to string 53 | data[col] = data[col].astype(str) 54 | if data[col].dtype != 'object': 55 | raise ValueError(f"Column '{col}' is not of type 'Plain Text'.") 56 | if worksheet == "Crew": 57 | for col in ['Team Name', 'Assignment', 'Process', 'Embedding model', 'Manager LLM']: 58 | data[col] = data[col].astype(str).apply(dedent).replace('None', None).replace('nan', None) 59 | for col in ['Verbose', 'Memory']: 60 | data[col] = data[col].astype(bool) 61 | data['t'] = data['t'].astype(float) 62 | data['num_ctx'] = data['num_ctx'].astype(int).replace(0, None) 63 | if worksheet == 'Tools': 64 | data.replace('None', None, inplace=True) 65 | except Exception as e: 66 | return e 67 | 68 | data = data.where(pd.notnull(data), None) # Replace NaN values with None 69 | 70 | # Append the DataFrame to the list of dataframes 71 | dataframes.append(data) 72 | 73 | return dataframes 74 | 75 | @staticmethod 76 | def parse_table(url=template_sheet_url): 77 | num_att = 0 78 | while num_att < 10: 79 | try: 80 | dataframes = Sheets.read_google_sheet(url) 81 | if isinstance(dataframes, Exception): 82 | raise dataframes 83 | break 84 | except ValueError as e: 85 | logger.error(f"ValueError occurred: {e}") 86 | print(f"Oops! Something went bonkers with the sheet. {e}") 87 | url = get_sheet_url_from_user() 88 | num_att += 1 89 | except URLError as e: 90 | logger.error(f"URLError occurred: {e}") 91 | print(f"Trying to open '{url}' and I'm all thumbs (which is sad because I don't have any)! Can you check that URL for me? {e}") 92 | url = get_sheet_url_from_user() 93 | num_att += 1 94 | else: 95 | print("10 attempts? Is this a new world record? I'm not equipped for marathons! Gotta hit the shutdown button now.") 96 | sys.exit(0) 97 | 98 | Agents = dataframes[0] 99 | Tasks = dataframes[1] 100 | Crew = dataframes[2] 101 | Models = dataframes[3] 102 | Tools = dataframes[4] 103 | 104 | return Agents, Tasks, Crew, Models, Tools 105 | 106 | # from sqlalchemy import create_engine 107 | # engine = create_engine('sqlite:///my_database.db') 108 | 109 | # # Write DataFrames to SQL tables 110 | # Agents.to_sql('Agents', con=engine, index=False, if_exists='replace') 111 | # Tasks.to_sql('Tasks', con=engine, index=False, if_exists='replace') 112 | # Crew.to_sql('Crew', con=engine, index=False, if_exists='replace') 113 | # Models.to_sql('Models', con=engine, index=False, if_exists='replace') 114 | # Tools.to_sql('Tools', con=engine, index=False, if_exists='replace') -------------------------------------------------------------------------------- /utils/tools_llm_config.py: -------------------------------------------------------------------------------- 1 | import os 2 | import logging 3 | import pandas as pd 4 | from utils.helpers import load_env 5 | logger = logging.getLogger(__name__) 6 | #load_env("../../ENV/.env", ["OPENAI_API_KEY",]) 7 | 8 | #Class to manage the configuration of the LLM to Tools 9 | class ConfigurationManager: 10 | def __init__(self, models_df): 11 | self.df = models_df 12 | 13 | def select(self, target_column, where_column, equals): 14 | try: 15 | result = self.df.loc[self.df[where_column] == equals, target_column].iloc[0] 16 | return result if not pd.isna(result) else None 17 | except IndexError: 18 | logger.info(f"No match found for {equals} in {where_column}") 19 | return None 20 | 21 | def get_model_details(self, model=None, embedding_model=None): 22 | # Default settings for OpenAI if specific model details are not provided 23 | 24 | details = { 25 | 'model' : model if model else 'gpt-4-turbo-preview', 26 | 'provider' : self.select('Provider', 'Model', model) if model else None, 27 | 'deployment_name' : self.select('Deployment', 'Model', model) if model else None, 28 | 'base_url' : self.select('base_url', 'Model', model) if model else None, 29 | 'api_key' : "NA", 30 | 'embedding_model' : embedding_model if embedding_model else 'text-embedding-3-small', 31 | 'provider_embedding' : self.select('Provider', 'Model', embedding_model) if embedding_model else None, 32 | 'deployment_name_embedding' : self.select('Deployment', 'Model', embedding_model) if embedding_model else None, 33 | 'base_url_embedding' : self.select('base_url', 'Model', embedding_model) if embedding_model else None, 34 | 'api_key_embedding' : "NA" 35 | } 36 | return details 37 | 38 | def build_config(self, model=None, embedding_model=None): 39 | details = self.get_model_details(model, embedding_model) 40 | config = { 41 | 'llm': { 42 | 'provider': details['provider'], 43 | 'config': { 44 | 'model': details['model'], 45 | 'deployment_name': details['deployment_name'], 46 | 'base_url': details['base_url'], 47 | 'temperature': 0.1, 48 | 'api_key': details['api_key'] 49 | } 50 | }, 51 | 'embedder': { 52 | 'provider': details['provider_embedding'], 53 | 'config': { 54 | 'model': details['embedding_model'], 55 | 'deployment_name': details['deployment_name_embedding'], 56 | 'base_url': details['base_url_embedding'], 57 | 'api_base': details['base_url_embedding'], 58 | 'api_key': details['api_key_embedding'] 59 | } 60 | } 61 | } 62 | return config 63 | 64 | def get_config(model=None, embedding_model=None, models_df=None): 65 | if not model and not embedding_model: 66 | return None 67 | 68 | config_manager = ConfigurationManager(models_df) 69 | config = config_manager.build_config(model=model, embedding_model=embedding_model) 70 | 71 | # Define actions based on provider specific settings. 72 | provider_actions = { 73 | 'azure_openai': ('base_url', 'api_base'), 74 | 'openai_compatible': ('deployment_name'), 75 | 'openai': ('deployment_name','base_url'), 76 | 'groq': ('deployment_name', ), 77 | 'ollama': ('api_base', 'api_key') 78 | } 79 | 80 | for component_key in ['llm', 'embedder']: 81 | component_config = config[component_key]['config'] 82 | provider = config[component_key]['provider'] 83 | 84 | #Provider specific settings: 85 | #Additional API key handling based on provider specifications 86 | if provider is None or pd.isna(provider): 87 | logger.info(f"No provider found for {component_key}") 88 | config.pop(component_key) 89 | continue 90 | if provider == 'openai_compatible': 91 | config[component_key]['config']['api_key'] = 'NA' #TODO: - get api_key from table 92 | elif provider == 'azure_openai' and config[component_key]['config']['api_key'] == "NA": 93 | config[component_key]['config']['api_key'] = os.environ["AZURE_OPENAI_KEY"] 94 | elif provider == 'openai': 95 | config[component_key]['config']['api_key'] = os.environ.get("SECRET_OPENAI_API_KEY") 96 | elif provider == 'ollama': 97 | #Nothin to do 98 | pass 99 | else: 100 | env_var = f"{provider.upper().replace('-', '_')}_API_KEY" 101 | config[component_key]['config']['api_key'] = os.environ.get(env_var) 102 | 103 | # Remove certain fields based on provider 104 | fields_to_remove = provider_actions.get(provider, []) 105 | for field in fields_to_remove: 106 | config[component_key]['config'].pop(field, None) 107 | 108 | # Fibnal cleanup - remove keys with None values directly within the dictionary comprehension 109 | config[component_key]['config'] = {k: v for k, v in component_config.items() if v is not None} 110 | 111 | return config 112 | 113 | #TODO: support all of the providers 114 | # LLM providers suppoerted by langchain: 'openai', 'azure_openai', 'anthropic', 'huggingface', 'cohere', 'together', 'gpt4all', 'ollama', 'jina', 'llama2', 'vertexai', 'google', 'aws_bedrock', 'mistralai', 'vllm', 'groq', 'nvidia' 115 | # Embedder providers supported by langchain: # 'openai', 'gpt4all', 'huggingface', 'vertexai', 'azure_openai', 'google', 'mistralai', 'nvidia'. 116 | 117 | # template: Optional[Template] = None, 118 | # prompt: Optional[Template] = None, 119 | # model: Optional[str] = None, 120 | # temperature: float = 0, 121 | # max_tokens: int = 1000, 122 | # top_p: float = 1, 123 | # stream: bool = False, 124 | # deployment_name: Optional[str] = None, 125 | # system_prompt: Optional[str] = None, 126 | # where: dict[str, Any] = None, 127 | # query_type: Optional[str] = None, 128 | # callbacks: Optional[list] = None, 129 | # api_key: Optional[str] = None, 130 | # base_url: Optional[str] = None, 131 | # endpoint: Optional[str] = None, 132 | # model_kwargs: Optional[dict[str, Any]] = None, 133 | # local: Optional[bool] = False, 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | -------------------------------------------------------------------------------- /utils/tools_mapping.py: -------------------------------------------------------------------------------- 1 | import logging 2 | logger = logging.getLogger(__name__) 3 | logger.debug(f"Entered {__file__}") 4 | from utils.helpers import load_env 5 | #load_env("../../ENV/.env", ["OPENAI_API_KEY","OPENAI_BASE_URL"]) 6 | from utils.callable_registry import CallableRegistry 7 | 8 | from utils.safe_argment_parser import parse_arguments 9 | from utils.tools_llm_config import get_config 10 | 11 | 12 | import os 13 | import re 14 | 15 | 16 | 17 | class ToolsMapping: 18 | def __init__(self, tools_df, models_df): 19 | self.tool_registry = CallableRegistry() ## Registry of all callables from all modules defined in tools_config.py... 20 | ## CallableRegistry itaretes through all allowed tool modules and registers all callables... 21 | ##...so that we can add tools by configuation in the tools_df 22 | self.tools = {} 23 | self.load_tools(tools_df, models_df) 24 | 25 | def load_tools(self, tools_df, models_df): 26 | """ Load tools from a DataFrame, considering only enabled tools. """ 27 | for idx, row in tools_df.iterrows(): 28 | if not row['On']: 29 | continue 30 | 31 | tool_name, class_or_func_details = row['Tool'], row['Class'] 32 | if class_or_func_details is None: 33 | logger.warning(f"No class or function found for tool '{tool_name}'. Tool not created.") 34 | continue 35 | 36 | # Extract the base function or class name and any indices or arguments 37 | base_name = re.sub(r"\(.*$", "", class_or_func_details).strip() 38 | arguments = re.search(r"\((.*)\)", class_or_func_details) 39 | index_match = re.search(r"\)\[(\d+)\]", class_or_func_details) 40 | # Get the callable from the registry 41 | logger.info(f"Loading tool '{tool_name}' with base name '{base_name}' and arguments '{arguments.group(1) if arguments else ''}'.") 42 | class_or_func = self.tool_registry.get_callable(base_name) 43 | if class_or_func is None: 44 | logger.warning(f"No callable found for '{base_name}'. Tool '{tool_name}' not created.") 45 | continue 46 | 47 | if isinstance(class_or_func, list) and index_match: 48 | index = int(index_match.group(1)) 49 | class_or_func = class_or_func[index] 50 | 51 | args, kwargs = parse_arguments(arguments.group(1) if arguments else '') 52 | if callable(class_or_func): 53 | if row['Model'] is not None or row['Embedding Model'] is not None: 54 | model = row['Model'] 55 | embedding_model = row['Embedding Model'] 56 | config = get_config(model=model, embedding_model=embedding_model, models_df=models_df) 57 | if config is not None: 58 | kwargs['config'] = config 59 | else: 60 | logger.info(f"'{tool_name}' does not have llm config.") 61 | 62 | logger.info(f"Creating tool '{tool_name}' with allable {class_or_func} and arguments '{args}' and keyword arguments '{kwargs}'.") 63 | #look at kwargs if ['congig]['llm'][provider] is "azure_openai" or ['congig]['embedder'][provider] is "azure_openai" load the enviroment variables 64 | # if 'config' in kwargs: 65 | # if 'llm' in kwargs['config']: 66 | # if 'provider' in kwargs['config']['llm']: 67 | # if kwargs['config']['llm']['provider'] == "azure_openai": 68 | # load_env("../../ENV/.env", ["OPENAI_API_KEY","OPENAI_BASE_URL"]) 69 | # if 'embedder' in kwargs['config']: 70 | # if 'provider' in kwargs['config']['embedder']: 71 | # if kwargs['config']['embedder']['provider'] == "azure_openai": 72 | #load_env("../../ENV/.env", ["OPENAI_API_KEY","OPENAI_BASE_URL"]) 73 | #TODO see if more processing is neede here. There is potentia; to change up env variables for each tool and stay in the 74 | #also print the config 75 | #print(f"ToolsMapping about to add kwargs callable '{tool_name}': {kwargs['config']}") 76 | self.tools[tool_name] = class_or_func(*args, **kwargs) 77 | else: 78 | logger.error(f"Callable for '{base_name}' is not a function or class constructor.") 79 | 80 | for tool_name, tool in self.tools.items(): 81 | if isinstance(tool, list) and tool: 82 | self.tools[tool_name] = tool[0] 83 | 84 | def get_tools(self): 85 | """ Retrieve the dictionary of all tools. """ 86 | return self.tools 87 | 88 | # TODO 89 | # def tool_wrapper(tool_func, max_output_size): 90 | # def wrapped_function(*args, **kwargs): 91 | # output = tool_func(*args, **kwargs) 92 | # output_str = str(output) 93 | # if len(output_str) > max_output_size: 94 | # return "This tool has exceeded the allowed size limit." 95 | # else: 96 | # return output 97 | # return wrapped_function 98 | # MAX_OUTPUT_SIZE=100 # Maximum size of output allowed for a tool 99 | 100 | # def wrap_class(original_class, args, *kwargs): 101 | # class WrappedClass(original_class): 102 | # def _run(self): 103 | # print("extra actions before _run") 104 | # result = super()._run(*args, **kwargs) 105 | # print("extra afctions after _run") 106 | # return result 107 | # WrappedClass.__name_ = original_class.__name_ 108 | # return WrappedClass 109 | 110 | 111 | # class ToolsMapping: 112 | # code_docs_search_tool = wrap_class(CodeDocsSearchTool) # A RAG tool optimized for searching through code documentation and related technical documents. 113 | # csv_search_tool = CSVSearchTool() # A RAG tool designed for searching within CSV files, tailored to handle structured data. 114 | # directory_search_tool = wrap_class(DirectorySearchTool) 115 | 116 | 117 | #old def Tools: just in case we need it 118 | # code_docs_search_tool = CodeDocsSearchTool(config=config) # A RAG tool optimized for searching through code documentation and related technical documents. 119 | # csv_search_tool = CSVSearchTool(config=config) # A RAG tool designed for searching within CSV files, tailored to handle structured data. 120 | # directory_search_tool = DirectorySearchTool(config=config) # A RAG tool for searching within directories, useful for navigating through file systems. 121 | # docx_search_tool = DOCXSearchTool(config=config) # A RAG tool aimed at searching within DOCX documents, ideal for processing Word files. 122 | # directory_read_tool = DirectoryReadTool(config=config) # Facilitates reading and processing of directory structures and their contents. 123 | # file_read_tool = FileReadTool() # Enables reading and extracting data from files, supporting various file formats. 124 | # #github_search_tool = GithubSearchTool(content_types=['code', 'repo', 'pr', 'issue']) 125 | # # Options: code, repo, pr, issue), #A RAG tool for searching within GitHub repositories, useful for code and documentation search. 126 | # # '#seper_dev_tool' :seper_dev_tool, # A specialized tool for development purposes, with specific functionalities under development. 127 | # txt_search_tool = TXTSearchTool(config=config) # A RAG tool focused on searching within text (.txt) files, suitable for unstructured data. 128 | # json_search_tool = JSONSearchTool(config=config) # A RAG tool designed for searching within JSON files, catering to structured data handling. 129 | # mdx_search_tool = MDXSearchTool(config=config) # A RAG tool tailored for searching within Markdown (MDX) files, useful for documentation. 130 | # pdf_search_tool = PDFSearchTool(config=config) # A RAG tool aimed at searching within PDF documents, ideal for processing scanned documents. 131 | # #pg_search_tool= PGSearchTool() #pg_search_tool, #A RAG tool optimized for searching within PostgreSQL databases, suitable for database queries. 132 | # rag_tool = RagTool(config=config) # A general-purpose RAG tool capable of handling various data sources and types. 133 | # scrape_element_from_website_tool= ScrapeElementFromWebsiteTool()# Enables scraping specific elements from websites, useful for targeted data extraction. 134 | # scrape_website_tool = ScrapeWebsiteTool() # Facilitates scraping entire websites, ideal for comprehensive data collection. 135 | # website_search_tool = WebsiteSearchTool(config=config) # A RAG tool for searching website content, optimized for web data extraction. 136 | # xml_search_tool = XMLSearchTool(config=config) # A RAG tool designed for searching within XML files, suitable for structured data formats. 137 | # youtube_channel_sarch_tool = YoutubeChannelSearchTool(config=config) # A RAG tool for searching within YouTube channels, useful for video content analysis. 138 | # youtube_video_saarch_tool = YoutubeVideoSearchTool(config=config) # A RAG tool aimed at searching within YouTube videos, ideal for video data extraction. 139 | # # langchain------------------------------------------ 140 | # human_tools = load_tools(["human"])[0] # A Tool that allows the Agents to talk to the n . 141 | # search = DuckDuckGoSearchResults(config=config) # Duck duck go Search 142 | # # crewai--sheets-ui 143 | # executor = CLITool.execute_cli_command # executor_tool, #interpreter CLI command 144 | # folder_tool = FolderTool.list_files # Lists all files into > a file so context is not spammed. Recursive option. 145 | # file_tool = FileTool() # Manages files with various actions including reading, appending text, editing lines, creating files, counting lines, and retrieving specific lines. xx 146 | # bs_code_tool = BSharpCodeTool() 147 | --------------------------------------------------------------------------------