DEV Community

Alain Airom
Alain Airom

Posted on

My personal “majordhomme” (butler) AI agent with CrewAI, Granite, DeepSeek and more…

An agent based on CrewAI with different skills using Granite and DeepSeek so far (more to come)

Image description

Introduction

This article describes a project I work on for my own learning and hands-on work on making agents and implementing different skills using divers tools. It is far from a professional class job, but I learn a lot while making my hands dirty and writing code on my own.

The main idea is to build an agent that during a chat, detects specific keywords, and based on the keywords uses set of tools implemented inside the application to propose and suggest additional services.

For example, if the user asks “Where is the capital of Russia”, the chat will obviously answer “Moscow” and then would suggest to the user if they want to know the weather in Moscow!

This is what I implemented so far and plan to expand to more set of tools to come (such as fetching airplane travel fees/timetables and maps directions and more…).

To do this I use the CrewAI framework. Services implemented so far are;

  • DeepSeek for general chat purpose (because I never tried it before),

Image description

  • granite-3.2–2b-instruct for using some tools (such as calling the weather API),

Image description

  • granite-vision-3.2–2b for describing images uploaded to the app.

Image description

  • I used Streamlit to build the GUI as by default it creates nice user interfaces.
  • The LLMs are accessed through Hugging Face.

So meet the ‘team’!

Image description

And the available and soon to be tools/options…

Image description

Implementation logique

As I mentioned in the introduction, the LLMs used are accessed through the Hugging Face platform, and so far I use 3 different LLM.

# Initialize Hugging Face Inference
client = InferenceClient(
    model="ibm-granite/granite-3.2-2b-instruct", token=HUGGINGFACE_API_TOKEN
)

vision_client = InferenceClient(
    model="ibm-granite/granite-vision-3.2-2b", token=HUGGINGFACE_API_TOKEN
)

# Initialize DeepSeek 
def initialize_deepseek_llm():
    llm = HuggingFaceEndpoint(
        repo_id="deepseek-ai/DeepSeek-R1-Distill-Qwen-32B",
        huggingfacehub_api_token=HUGGINGFACE_API_TOKEN,
        temperature=0.4,  
        max_new_tokens=512,
        #max_length=512,  
        stop_sequences=["\nQuestion:", "\nUser:"],
    )
    return llm
Enter fullscreen mode Exit fullscreen mode

Having implemented and tested each crew member individually, I wanted to establish a logic of collaboration between the members. So the idea became as the following schema describes it.

Image description

I had to try different types of attributes for DeepSeek in order to obtain what I wanted for my sample agent.

I have not bulletproofed the application (yet).

As shown below in the code which follows, I spent a lot of energy focusing on the use case of “city” name detection in the answer of the question “Where is the capital of XXX”, and did various tests on this use-case. The next step of my application would be to achieve a global solution. However so far the implementation is demonstrated in the code below.

For instance, I had to refine the prompt in order to have accurate answers for my ‘city’ use case.

def initialize_deepseek_prompt():
    template = """
    Question: What is the capital of France?
    Answer: Paris
    Question: What is the capital of Japan?
    Answer: Tokyo
    Question: What is the currency of Brazil?
    Answer: Brazilian Real
    Question: What is the tallest building in the world?
    Answer: Burj Khalifa
    Question: Provide only the name of the capital city of {question}
    Answer: """  
    prompt = PromptTemplate(template=template, input_variables=["question"])
    return prompt
Enter fullscreen mode Exit fullscreen mode

First things first, I use a virtual environment.

python3.12 -m venv crewai_env
source crewai_env/bin/activate
Enter fullscreen mode Exit fullscreen mode

Then goes all the requirements for the application.

streamlit
crewai
python-dotenv
requests
huggingface_hub
Pillow
spacy
langchain_community
langchain
langchain_huggingface
Enter fullscreen mode Exit fullscreen mode

And the credentials and other parameters needed for Hugging Face and other tools (one might need) in an ".env" file.

OPENWEATHERMAP_API_KEY="YOUR_KEY"
HUGGINGFACE_API_TOKEN="YOUR_TOKEN"
OPENAI_API_KEY="dummy_key"
Enter fullscreen mode Exit fullscreen mode

I have the habit now to isolate and test different functionalities in tiny code/apps to be able to prevent the upcoming probable problems. For instance here I test my Hugging Face connection.

from langchain.llms import HuggingFaceHub

try:
    llm = HuggingFaceHub(repo_id="google/flan-t5-small")
    print("HuggingFaceHub imported and initialized successfully.")
except NameError:
    print("NameError: HuggingFaceHub not found.")
except Exception as e:
    print(f"An error occurred: {e}")
Enter fullscreen mode Exit fullscreen mode

A simple test with “python app.py” lets me know if my token is correctly ‘sourced’ and if I can connect to HF platform.

Now, here is the looooong listing of the application.

import streamlit as st
import os
from crewai import Agent, Task, Crew, Process
from dotenv import load_dotenv
import requests
import json
import io
import base64
import time
import sys
import re
import spacy

nlp = spacy.load("en_core_web_sm")

from datetime import datetime
from huggingface_hub import InferenceClient
from PIL import Image
from langchain_huggingface import HuggingFaceEndpoint
from langchain.prompts import PromptTemplate
from langchain_core.runnables import RunnableSequence
from langchain_community.llms import HuggingFaceHub # Corrected import
from langchain.chains import LLMChain
from langchain.prompts import PromptTemplate


# Initializations
if "stop_app" not in st.session_state:
    st.session_state.stop_app = False

if st.button("Stop App"):
    st.session_state.stop_app = True

if st.session_state.stop_app:
    st.write("Stopping the app...")
    sys.exit()


if "nlp" not in st.session_state:
    st.session_state.nlp = spacy.load("en_core_web_sm")


# Load environment variables
load_dotenv()
OPENWEATHERMAP_API_KEY = os.getenv("OPENWEATHERMAP_API_KEY")
HUGGINGFACE_API_TOKEN = os.getenv("HUGGINGFACE_API_TOKEN")
GLOBAL_CITY = ""

#print(f"OpenWeatherMap API Key: {OPENWEATHERMAP_API_KEY}")
#print(f"Hugging Face API Token: {HUGGINGFACE_API_TOKEN}")


client = InferenceClient(
    model="ibm-granite/granite-3.2-2b-instruct", token=HUGGINGFACE_API_TOKEN

)

vision_client = InferenceClient(
    model="ibm-granite/granite-vision-3.2-2b", token=HUGGINGFACE_API_TOKEN

)


def initialize_deepseek_llm():
    llm = HuggingFaceEndpoint(
        repo_id="deepseek-ai/DeepSeek-R1-Distill-Qwen-32B",
        huggingfacehub_api_token=HUGGINGFACE_API_TOKEN,
        temperature=0.4,  
        max_new_tokens=512,
        #max_length=512,  
        stop_sequences=["\nQuestion:", "\nUser:"],
    )
    return llm

def initialize_deepseek_prompt():
    template = """
    Question: What is the capital of France?
    Answer: Paris
    Question: What is the capital of Japan?
    Answer: Tokyo
    Question: What is the currency of Brazil?
    Answer: Brazilian Real
    Question: What is the tallest building in the world?
    Answer: Burj Khalifa
    Question: Provide only the name of the capital city of {question}
    Answer: """  # Refined prompt
    prompt = PromptTemplate(template=template, input_variables=["question"])
    return prompt

def initialize_deepseek_chain(llm, prompt):
    llm_chain = RunnableSequence(prompt, llm)
    return llm_chain

if "deepseek_llm" not in st.session_state:
    st.session_state.deepseek_llm = initialize_deepseek_llm()
if "deepseek_prompt" not in st.session_state:
    st.session_state.deepseek_prompt = initialize_deepseek_prompt()
if "deepseek_chain" not in st.session_state:
    st.session_state.deepseek_chain = initialize_deepseek_chain(st.session_state.deepseek_llm, st.session_state.deepseek_prompt)


# --- Weather Forecast ---

def get_weather_forecast(city):

    url = f"http://api.openweathermap.org/data/2.5/forecast?q={city}&appid={OPENWEATHERMAP_API_KEY}&units=metric"
    try:
        response = requests.get(url)
        time.sleep(1) 
        response.raise_for_status()
        data = response.json()
        return data
    except requests.exceptions.RequestException as e:
        return {"error": f"Error fetching weather data: {e}"}

def format_forecast(forecast_data):
    """Formats the raw forecast data into a human-readable string."""
    if "error" in forecast_data:
        return forecast_data["error"]

    forecast_list = forecast_data["list"]
    daily_forecasts = {}

    for forecast in forecast_list:
        date_time = datetime.fromisoformat(forecast["dt_txt"])
        date = date_time.date()

        if date not in daily_forecasts:
            daily_forecasts[date] = {
                "temperatures": [],
                "descriptions": [],
                "humidity": [],
                "wind_speed": [],
            }

        daily_forecasts[date]["temperatures"].append(forecast["main"]["temp"])
        daily_forecasts[date]["descriptions"].append(forecast["weather"][0]["description"])
        daily_forecasts[date]["humidity"].append(forecast["main"]["humidity"])
        daily_forecasts[date]["wind_speed"].append(forecast["wind"]["speed"])

    formatted_forecast = ""
    for date, data in daily_forecasts.items():
        avg_temp = sum(data["temperatures"]) / len(data["temperatures"])
        description = ", ".join(set(data["descriptions"]))
        avg_humidity = sum(data["humidity"]) / len(data["humidity"])
        avg_wind_speed = sum(data["wind_speed"]) / len(data["wind_speed"])

        formatted_forecast += f"**{date.strftime('%Y-%m-%d')}**:\n"
        formatted_forecast += f"- Average Temperature: {avg_temp:.1f}°C\n"
        formatted_forecast += f"- Description: {description}\n"
        formatted_forecast += f"- Average Humidity: {avg_humidity:.1f}%\n"
        formatted_forecast += f"- Average Wind Speed: {avg_wind_speed:.1f} m/s\n\n"

    return formatted_forecast

def generate_response(prompt, max_retries=3, retry_delay=5):
    retries = 0
    while retries < max_retries:
        try:

            response = client.text_generation(prompt, max_new_tokens=250)
            return response
        except requests.exceptions.RequestException as e:
            if "503 Server Error" in str(e):
                retries += 1
                print(f"Retry {retries}/{max_retries}. Error: {e}")
                time.sleep(retry_delay)
            else:
                return f"Error generating response: {e}"
        except Exception as e:
            return "Hugging Face is temporarily unavailable. Please try again later."
    return "Maximum retries exceeded. Hugging Face is not responding. Please try again later."


weather_fetcher = Agent( 
    role="Weather Data Fetcher",
    goal="Fetch weather forecast data from OpenWeatherMap API",
    backstory="You are an expert in retrieving weather data from online APIs.",
    allow_delegation=False,
    verbose=True,
    llm=None,
    tools=[],
)    


weather_formatter = Agent(
    role="Weather Forecast Formatter",
    goal="Format raw weather data into a readable forecast",
    backstory="You are skilled at presenting complex weather data in a clear and concise way.",
    allow_delegation=False,
    verbose=True,
    llm=None,
    tools=[],
)


fetch_weather_task = Task(
    description="Fetch the 7-day weather forecast for the specified city.",
    agent=weather_fetcher,
    process=Process.sequential,
    expected_output="JSON Weather Data",
)

format_weather_task = Task(
    description="Format the raw weather data into a detailed and easy-to-understand forecast.",
    agent=weather_formatter,
    process=Process.sequential,
    expected_output="Formatted Weather Forecast",
)


class WeatherCrew:
    def __init__(self):
        self.crew = Crew(
            agents=[weather_fetcher, weather_formatter],
            tasks=[fetch_weather_task, format_weather_task],
            verbose=True,
            process=Process.sequential,
        )

    def get_forecast(self, city):

        forecast_data = get_weather_forecast(city)
        if "error" in forecast_data:
            return forecast_data["error"]

        formatted_data = format_forecast(forecast_data)
        prompt = f"Here is weather data: {formatted_data}. Please provide a summary."

        result = generate_response(prompt)
        return result

# --- Image Description ---

def describe_image(image_bytes):

    try:

        response = vision_client.image_to_text(image_bytes)
        return response
    except Exception as e:
        return f"Error describing image: {e}"

# --- Streamlit UI ---

def weather_app():
    st.title("Weather Forecast App")
    st.image("./icons/weather-news.png", width=100)
    city = st.text_input("Enter city name:")

    if st.button("Get Weather Forecast"):
        if city:
            with st.spinner("Fetching forecast..."):
                weather_crew = WeatherCrew()
                forecast = weather_crew.get_forecast(city)
                if "Hugging Face" in forecast: 
                    st.error(forecast) 
                else:
                    st.write(forecast)
        else:
            st.warning("Please enter a city name.")

def image_app():
    st.header("Image Description")
    st.image("./icons/camera.png", width=100)
    uploaded_file = st.file_uploader("Upload an image", type=["jpg", "jpeg", "png"])

    if uploaded_file is not None:
        image_bytes = uploaded_file.getvalue()
        st.image(image_bytes, caption="Uploaded Image.", use_container_width=True) # change use_column_width to use_container_width

        if st.button("Describe Image"):
            with st.spinner("Describing image..."):
                description = describe_image(image_bytes)
                if "Hugging Face" in description:
                    st.error(description)
                else:
                    st.write(description)

def chat_app():
    st.header("General Chat with DeepSeek")
    st.image("./icons/deepseek-logo-03.png", width=100)
    user_input = st.text_input("Enter your query: ")


    if "city_detected" not in st.session_state:
        st.session_state.city_detected = False
    if "last_mentioned_city" not in st.session_state:
        st.session_state.last_mentioned_city = None
    if "weather_prompt" not in st.session_state:
        st.session_state.weather_prompt = None
    if "weather_query" not in st.session_state:
        st.session_state.weather_query = False  

    if user_input:
        if "weather" in user_input.lower():

            if "last_mentioned_city" in st.session_state and st.session_state.last_mentioned_city:
                city = st.session_state.last_mentioned_city
                st.write(f"Using last_mentioned_city: {city}")  # Debug
            elif "that city" in user_input.lower() and "last_city" in st.session_state:
                city = st.session_state.last_city
                st.write(f"Using last_city: {city}")  # Debug
            else:
                match = re.search(r"weather\s+in\s+(.+?) (?:\s*\?)?$", user_input, re.IGNORECASE)
                if match:
                    city = match.group(1).strip()
                else:
                    city = None
            if city:
                with st.spinner("Fetching weather forecast..."):
                    weather_crew = WeatherCrew()
                    forecast = weather_crew.get_forecast(city)
                    if "Hugging Face" in forecast:
                        st.error(forecast)
                    else:
                        st.write(forecast)
            else:
                st.warning("Please specify a city after 'weather in'.")
            return

        with st.spinner("Generating response..."):
            response = st.session_state.deepseek_chain.invoke({"question": user_input})
        st.write(f"LLM Response: {response}")
        response = re.sub(r"<think>.*?</think>", "", response, flags=re.DOTALL)
        st.text_area("DeepSeek:", value=response, height=200)

        # Extract city name from DeepSeek response
        city = extract_city(response)
        st.write(f"Extracted City: {city}")  # Debug: 

        update_city_state(city)

        display_weather_option(city)  # Display weather option

        if st.session_state.weather_query and st.session_state.get("weather_prompt"):
            # Use the last mentioned city for the weather query
            city_to_use = st.session_state.last_mentioned_city
            st.write(f"Using city for weather: {city_to_use}") #Debug
            with st.spinner(f"Fetching weather for {city_to_use}..."):
                weather_crew = WeatherCrew()
                forecast = weather_crew.get_forecast(city_to_use)
                if "Hugging Face" in forecast:
                    st.error(forecast)
                else:
                    st.write(forecast)
            st.session_state.weather_query = False
            st.session_state.weather_prompt = None


if "switch_to_weather_app" in st.session_state and st.session_state.switch_to_weather_app:
    weather_app()
    st.session_state.switch_to_weather_app = False


if "switch_to_weather_app" in st.session_state and st.session_state.switch_to_weather_app:
    weather_app()
    st.session_state.switch_to_weather_app = False

def extract_city_ner(response):
    doc = nlp(response)
    for ent in doc.ents:
        if ent.label_ == "GPE":  # GPE = Geopolitical Entity 
            return ent.text
    return None


def extract_city(response):
    """
    Extracts a city name from the LLM's response
    """
    nlp = st.session_state.nlp  


    cities_found = re.findall(r"([A-Z][a-z]+(?: [A-Z][a-z]+)?)", response)
    if cities_found:
        answer_text = cities_found[-1]  # Get the last one
    else:
        answer_text = response  # Fallback


    doc = nlp(answer_text)
    for ent in doc.ents:
        if ent.label_ == "GPE":
            return ent.text
    return None

def update_city_state(city):


    if "city_detected" not in st.session_state:
        st.session_state.city_detected = False
    if "last_mentioned_city" not in st.session_state:
        st.session_state.last_mentioned_city = None


    if city:
        st.session_state.last_mentioned_city = city
        st.write(f"Updated last_mentioned_city: {st.session_state.last_mentioned_city}")
    else:
        st.write(f"last_mentioned_city not updated, current value: {st.session_state.last_mentioned_city}")

def display_weather_option(city):

    if st.session_state.get("weather_prompt") != city:
        st.write(f"Did you want to know the weather in {city}?")
        st.session_state.city_detected = True

    if st.session_state.city_detected:
        st.session_state.weather_query = True
        st.session_state.city_detected = False
        st.session_state.weather_prompt = city  
    else:
        st.session_state.city_detected = True

    if st.session_state.weather_query and st.session_state.weather_prompt:  # Use weather_prompt directly

        city_to_use = st.session_state.last_mentioned_city
        with st.spinner (f"Fetching weather for {city_to_use}..."):
            weather_crew = WeatherCrew()
            forecast = weather_crew.get_forecast(city_to_use)
            if "Hugging Face" in forecast:
                st.error(forecast)
            else:
                st.write(forecast)
        st.session_state.weather_query = False
        st.session_state.weather_prompt = None      


def hotel_app():  
    st.header("--- Hotel Booking Agent to come --")  
    st.image("./icons/hotel.png", width=100)

def airplane_app():  
    st.header("--- Airplane Booking Helper agent --")    
    st.image("./icons/avion.png", width=100)  

def dummy_app():  
    st.header("--- application to come --")                

# --- Main App ---
st.markdown(
    '<p style="font-size: 30px;">Set of Agents to manage your day-to-day tasks, 24/7!<br>'
    'They will work together very shortly ;p)</p>',
    unsafe_allow_html=True,
)
st.sidebar.image("./icons/robot.png", width=80) 
st.sidebar.image("./icons/1_GlpCVWSHwX7U7wwBaUnZFA.webp", width=80) 
st.sidebar.image("./icons/1_nIobR8OHlk0UqHIRQtnsww.webp", width=80) 
with st.expander("About the Team:"):
        st.subheader("Diagram")
        left_co, cent_co,last_co = st.columns(3)
        with cent_co:
            st.image("./icons/robot.png", width=80)

        st.subheader("Le majordome ")
        st.text("""       
        Role = Answer general questions
        Goal = Answer general questions
        Backstory = He knows almost everything.
        Task = Answer your queries, whatever they are. """)

        st.subheader("Weather forecaster")
        st.text("""       
        Role = Weather forecaster
        Goal = Fetch the weather of the place you ask for!
        Backstory = Uses IBM Granite to fetch the weather.
        Task = Search the weather of every possible place on earth for you.""")

        st.subheader("Image analyzer")
        st.text("""       
        Role = Analyze images for you 
        Goal= Gives you explanations on images you upload!
        Backstory = Uses IBM Granite to describe an image.
        Task = Summarize and explain an image. """)

        st.subheader("--- TBN ---")
        st.text("""       
        Role = TBN 
        Goal= TBN
        Backstory = TBN.
        Task = TBN. """)

app_mode = st.sidebar.selectbox("Select App Mode", 
                                ["General Chat", 
                                 "Weather Forecast", 
                                 "Image Description", 
                                 "Hotel Reservation", 
                                 "Airline Travel Search"]
                                )

if app_mode == "Weather Forecast":
    weather_app()
elif app_mode == "Image Description":
    image_app()
elif app_mode == "General Chat":
    chat_app()    
elif app_mode == "Hotel Reservation":
    hotel_app()
elif app_mode == "Airline Travel Search":
    airplane_app()    
Enter fullscreen mode Exit fullscreen mode

At the end, I was able to make the application work in the way I wanted as shown in the following screen captures.

Image description

And the weather forecast for the city.

Image description

Thanks for reading, appreciate feedbacks 🙏

Conclusion

This sample application implements a specific pre-determined keyword detection in a chat, and implements an intelligent agent which would provide in depth information regarding the detected keyword.

Hostinger image

Get n8n VPS hosting 3x cheaper than a cloud solution

Get fast, easy, secure n8n VPS hosting from $4.99/mo at Hostinger. Automate any workflow using a pre-installed n8n application and no-code customization.

Start now

Top comments (0)

Billboard image

Create up to 10 Postgres Databases on Neon's free plan.

If you're starting a new project, Neon has got your databases covered. No credit cards. No trials. No getting in your way.

Try Neon for Free →

👋 Kindness is contagious

Please leave a ❤️ or a friendly comment on this post if you found it helpful!

Okay