├── .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 | 
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 |
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 |
--------------------------------------------------------------------------------