#2 crewAI: prepare-se para a Rebelião das Máquinas!

Luciano Santos Borges
9 min readMay 23, 2024

--

TL;DR: Neste segundo artigo da série, vamos aprofundar nosso projeto no crewAI, focado na criação de uma equipe responsável por elaborar um plano de viagem. Abordaremos a criação de novas ferramentas, agentes e tarefas mais complexas. Se você ainda não leu o primeiro artigo, recomendo que o faça.

Após uma discussão enriquecedora no grupo de WhatsApp do Canal Sandeco e uma revisão da aula sobre colaboração multi-agentes do curso “Multi AI Agent System with crewAI” do João Moura, percebi que a estrutura hierárquica dos agentes será interessante apenas quando todos os agentes estiverem criados. Portanto, para seguir em frente, adotarei a abordagem sequencial. Segundo a documentação do crewAI, essa abordagem executa as tarefas na ordem predefinida na lista, onde a saída de uma tarefa serve como contexto para a próxima. E o desenho mais adequado, para agora, seria a ordem das tarefas, e não a hierarquia dos agentes. Neste artigo, seguiremos a sequência conforme a imagem abaixo, e nos próximos artigos desta série, continuaremos a evoluir esse processo.

Antes de prosseguirmos com a criação dos novos itens no projeto crewAI, gostaria de informar que adaptei o código do serviço REST criado no artigo anterior. Simplifiquei-o para evitar que os agentes se confundissem com informações que ainda não eram necessárias. A principal mudança é que o serviço agora retorna apenas o nome do cliente e o itinerário desejado. Veja como ficou o código:

# Instalar Flask e pyngrok
!pip install Flask Flask-SQLAlchemy pyngrok

# Importar as bibliotecas necessárias
from flask import Flask, request, jsonify
from flask_sqlalchemy import SQLAlchemy
from pyngrok import ngrok
from sqlalchemy.dialects.postgresql import ARRAY
from datetime import date


# Autenticar ngrok com seu token
!ngrok authtoken <put_your_ngrok_key_here>

# Inicializar a aplicação Flask
app = Flask(__name__)

# Configurar a URL do banco de dados SQLite
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///test.db'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False

# Inicializar o banco de dados
db = SQLAlchemy(app)

# Definir o modelo de dados Trip
class Trip(db.Model):
id = db.Column(db.Integer, primary_key=True)
client_name = db.Column(db.String(80), nullable=False)
itinerary = db.Column(db.PickleType, nullable=True) # Usando PickleType para armazenar arrays

# Criar o banco de dados e a tabela com o contexto de aplicação
with app.app_context():
db.create_all()

# Adicionar registros iniciais
if not Trip.query.first():
trip1 = Trip(
client_name="Wonder Woman",
itinerary=["Lisboa", "Valladolid", "Lourdes", "Nice", "Marseille", "Saragoça", "Madrid", "Lisboa"]
)
db.session.add(trip1)
db.session.commit()

# Definir uma rota para retornar uma viagem específica
@app.route('/trips/<int:id>', methods=['GET'])
def get_trip(id):
trip = Trip.query.get_or_404(id)
result = {
"id": trip.id,
"client_name": trip.client_name,
"itinerary": trip.itinerary
}
return jsonify(result), 200

# Definir uma rota simples
@app.route('/')
def hello_world():
return "Hello, World!"

# Rodar a aplicação Flask
if __name__ == '__main__':
# Configurar o ngrok para criar um túnel para a aplicação Flask
url = ngrok.connect(5000)
print('Public URL:', url)
app.run(port=5000)

Depois de toda essa introdução, vamos ao que interessa! A próxima etapa é criar um novo agente, chamado Agente de Roteamento, que será responsável por calcular a distância entre cidades e o tempo de viagem de carro entre elas. Para isso, desenvolveremos uma nova ferramenta personalizada que acessará o OpenRouteService para realizar esses cálculos.

O primeiro passo é criar sua chave no OpenRouteService e configurá-la no código, de maneira semelhante ao que fizemos com o Groq.

# Conexão ao OpenRoute Service
ors = openrouteservice.Client(key="<cole_sua_key_aqui>")

O segundo passo foi criar uma ferramenta para calcular a distância entre as cidades do itinerário e o tempo de viagem entre elas. Note que a ferramenta recebe o itinerário do contexto da atividade executada pelo Agente de Turismo, que obteve esse itinerário na API de um serviço REST. Nos trechos de código onde você vê “ors”, é o momento em que a ferramenta está chamando o OpenRouteService. Ressalto que o objetivo aqui não é se tornar um especialista em geoprocessamento 🌍, mas sim aprender como criar ferramentas ⚒️ customizadas.

# Função auxiliar que será usada pela ferramenta.
def get_city_coordinates(city_names):
coordinates = {}
for city in city_names:
geocode = ors.pelias_search(text=city)
if geocode['features']:
coordinates[city] = geocode['features'][0]['geometry']['coordinates']
return coordinates

@tool("Buscar a distância entre as cidades seguindo a ordem do itinerário.")
def route_tool(itinerary: list) -> list:
"""
Uma ferramenta para buscar a distância em km entre as cidades do itinerário.

Essa função faz uma requisição à API do OpenRouteService para recuperar a rotas entre duas cidades.

Em caso de sucesso, a função retorna uma lista composta por objetos json no formato:
"{'from': from_city, 'to': to_city, 'distance_km': distance_km, 'duration_hours': duration_hours}”.

Se a solicitação falhar por algum motivo, como problemas de conexão ou erros de API,
a função retornará uma mensagem de erro indicando o problema encontrado.
"""

# Obtendo as coordenadas das cidades
city_coordinates = get_city_coordinates(itinerary)

# Calculando a distância e duração para cada par de cidades adjacentes
results = []
num_cities = len(itinerary)
for i in range(num_cities - 1):
from_city = itinerary[i]
to_city = itinerary[i + 1]

# Obtendo as coordenadas das cidades adjacentes
from_coordinates = city_coordinates[from_city]
to_coordinates = city_coordinates[to_city]

# Fazendo a solicitação para calcular a distância e duração entre as cidades adjacentes
route = ors.directions(
coordinates=[from_coordinates, to_coordinates],
profile='driving-car',
format='geojson'
)

# Extraindo a distância e duração da rota
distance_km = route['features'][0]['properties']['segments'][0]['distance'] / 1000 # Convertendo para quilômetros
duration_hours = route['features'][0]['properties']['segments'][0]['duration'] / 3600 # Convertendo para horas

# Adicionando os resultados à lista
result = {
"from": from_city,
"to": to_city,
"distance_km": distance_km,
"duration_hours": duration_hours
}
results.append(result)

return results

O terceiro passo é criar o agente em si. Inicialmente, eu havia pensado no objetivo (goal) abaixo para ele, mas depois o tornei mais especializado, já que também limitei o retorno do serviço. No entanto, não se desiluda: assim que todos os agentes estiverem prontos com suas respectivas tarefas e ferramentas, vou tentar estender o objetivo dele.

Calcular a melhor rota de ida e volta entre a origem e o destino final da viagem, considerando o limite de distância a percorrer por dia e as cidades de interesse.

O código do Agente de Roteamento ficou assim:

route_agent = Agent(
role="Agente de Roteamento",
goal="Calcular apenas a distância e a duração entre os itens adjacentes do itinerário.",
backstory="Com uma vasta experiência em mapeamento e navegação, o agente "
"de roteamento utiliza algoritmos avançados para garantir que "
"os clientes tenham a rota mais eficiente e segura para sua viagem.",
allow_delegation=False,
tools=[route_tool],
verbose=True,
llm=llama3

Vale ressaltar que também alterei o allow_delegation para false do Agente de Viagem.

O quarto passo foi criar duas tarefas: uma para calcular a distância e o tempo de viagem entre as cidades, que será executada pelo Agente de Roteamento, e outra para consolidar os dados da viagem, onde o processo retorna para o Agente de Turismo. É importante destacar que o contexto da atividade de calcular a distância e o tempo de viagem é fornecido pela primeira tarefa executada pelo Agente de Turismo, que busca o itinerário de uma determinada viagem.

calculate_distance_and_travel_time_task = Task(
description="Descobrir a distância e a duração da viagem de carro entre as cidades.",
expected_output="Lista com a distância e a duração da viagem de carro entre as cidades do itinerário.",
tools = [route_tool],
agent = route_agent,
context=[load_trip_task]
)

consolidate_trip = Task (
description="Consolidar os dados da viagem do cliente.",
expected_output="Dados da viagem em estilo markdown.",
agent = trip_agent
)

O passo final é adicionar este novo agente e as novas tarefas à equipe do crewAI.

# Crew
trip_crew = Crew(
agents=[trip_agent, route_agent],
tasks=[load_trip_task, calculate_distance_and_travel_time_task, consolidate_trip],
process=Process.sequential,
verbose=True
)

O código final do serviço e do crewAI está disponível no repositório https://github.com/lusabo/crewai. Os arquivos relevantes para esta série são: crewai_service.ipynb e crewai_trip.ipynb.

Aqui tá o console final depois de rodar essa Crew!


[DEBUG]: == Working Agent: Agente de Viagem
[INFO]: == Starting Task: Analisar a requisição cujo id é 1 para extrair os detalhes a fim de montar o melhor roteiro de viagem.


> Entering new CrewAgentExecutor chain...
Thought: I need to access the trip request data to plan the best route.

Action: Buscar dados da viagem via API
Action Input: {'trip_id': '1'}

{'client_name': 'Wonder Woman', 'id': 1, 'itinerary': ['Lisboa', 'Valladolid', 'Lourdes', 'Nice', 'Marseille', 'Saragoça', 'Madrid', 'Lisboa']}

Thought: I now know the final answer
Final Answer: {'client_name': 'Wonder Woman', 'id': 1, 'itinerary': ['Lisboa', 'Valladolid', 'Lourdes', 'Nice', 'Marseille', 'Saragoça', 'Madrid', 'Lisboa']}

> Finished chain.
[DEBUG]: == [Agente de Viagem] Task output: {'client_name': 'Wonder Woman', 'id': 1, 'itinerary': ['Lisboa', 'Valladolid', 'Lourdes', 'Nice', 'Marseille', 'Saragoça', 'Madrid', 'Lisboa']}


[DEBUG]: == Working Agent: Agente de Roteamento
[INFO]: == Starting Task: Descobrir a distância e a duração da viagem de carro entre as cidades.


> Entering new CrewAgentExecutor chain...
I need to calculate the distance and duration between each pair of adjacent cities in the itinerary.

Action: Buscar a distância entre as cidades seguindo a ordem do itinerário.
Action Input: {"itinerary": ["Lisboa", "Valladolid", "Lourdes", "Nice", "Marseille", "Saragoça", "Madrid", "Lisboa"]}

[{'from': 'Lisboa', 'to': 'Valladolid', 'distance_km': 583.6294, 'duration_hours': 5.549777777777778}, {'from': 'Valladolid', 'to': 'Lourdes', 'distance_km': 544.327, 'duration_hours': 5.3885}, {'from': 'Lourdes', 'to': 'Nice', 'distance_km': 728.1339, 'duration_hours': 7.046583333333333}, {'from': 'Nice', 'to': 'Marseille', 'distance_km': 202.574, 'duration_hours': 2.1871388888888887}, {'from': 'Marseille', 'to': 'Saragoça', 'distance_km': 786.036, 'duration_hours': 7.627972222222223}, {'from': 'Saragoça', 'to': 'Madrid', 'distance_km': 307.5577, 'duration_hours': 3.171111111111111}, {'from': 'Madrid', 'to': 'Lisboa', 'distance_km': 633.9265, 'duration_hours': 6.0145277777777775}]

Thought:
I have the distances and durations between each pair of adjacent cities in the itinerary.

Final Answer:
[{'from': 'Lisboa', 'to': 'Valladolid', 'distance_km': 583.6294, 'duration_hours': 5.549777777777778},
{'from': 'Valladolid', 'to': 'Lourdes', 'distance_km': 544.327, 'duration_hours': 5.3885},
{'from': 'Lourdes', 'to': 'Nice', 'distance_km': 728.1339, 'duration_hours': 7.046583333333333},
{'from': 'Nice', 'to': 'Marseille', 'distance_km': 202.574, 'duration_hours': 2.1871388888888887},
{'from': 'Marseille', 'to': 'Saragoça', 'distance_km': 786.036, 'duration_hours': 7.627972222222223},
{'from': 'Saragoça', 'to': 'Madrid', 'distance_km': 307.5577, 'duration_hours': 3.171111111111111},
{'from': 'Madrid', 'to': 'Lisboa', 'distance_km': 633.9265, 'duration_hours': 6.0145277777777775}]

> Finished chain.
[DEBUG]: == [Agente de Roteamento] Task output: [{'from': 'Lisboa', 'to': 'Valladolid', 'distance_km': 583.6294, 'duration_hours': 5.549777777777778},
{'from': 'Valladolid', 'to': 'Lourdes', 'distance_km': 544.327, 'duration_hours': 5.3885},
{'from': 'Lourdes', 'to': 'Nice', 'distance_km': 728.1339, 'duration_hours': 7.046583333333333},
{'from': 'Nice', 'to': 'Marseille', 'distance_km': 202.574, 'duration_hours': 2.1871388888888887},
{'from': 'Marseille', 'to': 'Saragoça', 'distance_km': 786.036, 'duration_hours': 7.627972222222223},
{'from': 'Saragoça', 'to': 'Madrid', 'distance_km': 307.5577, 'duration_hours': 3.171111111111111},
{'from': 'Madrid', 'to': 'Lisboa', 'distance_km': 633.9265, 'duration_hours': 6.0145277777777775}]


[DEBUG]: == Working Agent: Agente de Viagem
[INFO]: == Starting Task: Consolidar os dados da viagem do cliente.


> Entering new CrewAgentExecutor chain...
I now can give a great answer!

Final Answer:

**Travel Itinerary**

### Leg 1: Lisboa to Valladolid

* Distance: 583.63 km
* Duration: 5 hours 33 minutes

### Leg 2: Valladolid to Lourdes

* Distance: 544.33 km
* Duration: 5 hours 23 minutes

### Leg 3: Lourdes to Nice

* Distance: 728.13 km
* Duration: 7 hours 2 minutes

### Leg 4: Nice to Marseille

* Distance: 202.57 km
* Duration: 2 hours 11 minutes

### Leg 5: Marseille to Saragoça

* Distance: 786.04 km
* Duration: 7 hours 37 minutes

### Leg 6: Saragoça to Madrid

* Distance: 307.56 km
* Duration: 3 hours 10 minutes

### Leg 7: Madrid to Lisboa

* Distance: 633.93 km
* Duration: 6 hours 1 minute

**Total Distance:** 3,485.19 km
**Total Duration:** 36 hours 19 minutes

> Finished chain.
[DEBUG]: == [Agente de Viagem] Task output: **Travel Itinerary**

### Leg 1: Lisboa to Valladolid

* Distance: 583.63 km
* Duration: 5 hours 33 minutes

### Leg 2: Valladolid to Lourdes

* Distance: 544.33 km
* Duration: 5 hours 23 minutes

### Leg 3: Lourdes to Nice

* Distance: 728.13 km
* Duration: 7 hours 2 minutes

### Leg 4: Nice to Marseille

* Distance: 202.57 km
* Duration: 2 hours 11 minutes

### Leg 5: Marseille to Saragoça

* Distance: 786.04 km
* Duration: 7 hours 37 minutes

### Leg 6: Saragoça to Madrid

* Distance: 307.56 km
* Duration: 3 hours 10 minutes

### Leg 7: Madrid to Lisboa

* Distance: 633.93 km
* Duration: 6 hours 1 minute

**Total Distance:** 3,485.19 km
**Total Duration:** 36 hours 19 minutes


**Travel Itinerary**

### Leg 1: Lisboa to Valladolid

* Distance: 583.63 km
* Duration: 5 hours 33 minutes

### Leg 2: Valladolid to Lourdes

* Distance: 544.33 km
* Duration: 5 hours 23 minutes

### Leg 3: Lourdes to Nice

* Distance: 728.13 km
* Duration: 7 hours 2 minutes

### Leg 4: Nice to Marseille

* Distance: 202.57 km
* Duration: 2 hours 11 minutes

### Leg 5: Marseille to Saragoça

* Distance: 786.04 km
* Duration: 7 hours 37 minutes

### Leg 6: Saragoça to Madrid

* Distance: 307.56 km
* Duration: 3 hours 10 minutes

### Leg 7: Madrid to Lisboa

* Distance: 633.93 km
* Duration: 6 hours 1 minute

**Total Distance:** 3,485.19 km
**Total Duration:** 36 hours 19 minutes

Chegamos ao fim de mais um artigo! Espero que este conteúdo tenha sido útil e inspirador para você. Se você gostou do que leu, por favor, deixe um like e compartilhe com outros que possam se beneficiar.

--

--