Salta el contingut

Pràctica 1 — El Primer Agent amb LangChain

📋 Fitxa de la Pràctica

⏱️ Durada: 3 hores 🎯 RA: RA2 (CA 2.1, CA 2.2) 📊 Pes: 15% de la nota pràctica 🐍 Python 3.11+ 🦜 LangChain 0.3.x

🎯 Objectius

Al finalitzar aquesta pràctica, l'alumne/a serà capaç de:

  • Configurar un entorn Python per a LangChain
  • Implementar un agent bàsic amb un LLM i una eina de cerca
  • Interpretar el raonament intern d'un agent (verbose)
  • Afegir eines personalitzades amb el decorador @tool
  • Gestionar errors i límits de l'agent

🔧 Configuració de l'Entorn

✅ Prerequisits

  • Python 3.11 o superior instal·lat
  • Una clau d'API d'OpenAI (o Anthropic com a alternativa)
  • VS Code o PyCharm instal·lat
  • Connexió a internet

Pas 1: Crear l'entorn virtual

# Crear un directori per al projecte
mkdir practica1-primer-agent
cd practica1-primer-agent

# Crear l'entorn virtual
python -m venv .venv

# Activar l'entorn (Linux/macOS)
source .venv/bin/activate

# Activar l'entorn (Windows)
.venv\Scripts\activate

# Verificar que estem a l'entorn correcte
which python    # Linux/macOS → hauria de mostrar la ruta de .venv
where python    # Windows

Pas 2: Instal·lar les dependències

# Instal·lar les llibreries necessàries
pip install langchain==0.3.7 \
            langchain-openai==0.2.6 \
            langchain-community==0.3.7 \
            duckduckgo-search==6.3.7 \
            python-dotenv==1.0.1

# Verificar la instal·lació
python -c "import langchain; print(f'LangChain {langchain.__version__}')"

Gestió de versions

Les versions especificades estan testades i funcionen correctament juntes. Si uses versions més noves, pot haver canvis en l'API. Consulta el CHANGELOG de LangChain.

Pas 3: Configurar les claus d'API

# Crear el fitxer de variables d'entorn (NO pujar mai a git!)
touch .env
echo "OPENAI_API_KEY=sk-proj-xxxxxxxxxxxx" >> .env

# Crear el .gitignore per protegir les claus
echo ".env" >> .gitignore
echo ".venv/" >> .gitignore
echo "__pycache__/" >> .gitignore

SEGURETAT CRÍTICA

MAI posis les teves claus d'API directament al codi. Usa sempre variables d'entorn. Una clau d'API exposada a GitHub pot generar factures de milers d'euros en pocs minuts si un bot maliciós la troba.


💻 Part 1: Agent Bàsic (60 minuts)

Crea el fitxer agent_basic.py:

"""
Pràctica 1 — Part 1: Agent Bàsic amb LangChain
Curs d'Agents d'IA — CFGS ASIX/DAW
"""

import os
from dotenv import load_dotenv
from langchain_openai import ChatOpenAI
from langchain.agents import create_react_agent, AgentExecutor
from langchain_community.tools import DuckDuckGoSearchResults
from langchain import hub

# Cargar variables d'entorn
load_dotenv()

# ─── CONFIGURACIÓ ────────────────────────────────────────────────
# Verificar que tenim la clau d'API
api_key = os.getenv("OPENAI_API_KEY")
if not api_key:
    raise ValueError(
        "No s'ha trobat OPENAI_API_KEY. "
        "Assegura't de crear el fitxer .env amb la clau."
    )

# ─── 1. MODEL DE LLENGUATGE ──────────────────────────────────────
llm = ChatOpenAI(
    model="gpt-4o-mini",    # Model econòmic per a proves
    temperature=0,           # 0 = determinista (millor per a agents)
    max_tokens=1500,         # Limitar tokens de resposta
    api_key=api_key
)

print("✅ Model carregat correctament")

# ─── 2. EINES ────────────────────────────────────────────────────
search_tool = DuckDuckGoSearchResults(
    name="cerca_web",
    description=(
        "Cerca informació actual a internet. "
        "Usa aquesta eina per a preguntes sobre events recents, "
        "novetats tecnològiques, o qualsevol informació que pugui "
        "haver canviat recentment. "
        "Entrada: string amb la cerca en català o castellà."
    ),
    num_results=3   # Nombre de resultats a retornar
)

tools = [search_tool]
print(f"✅ {len(tools)} eines configurades")

# ─── 3. PROMPT DE REACT ──────────────────────────────────────────
# El prompt de ReAct instrueix el model a raonar pas a pas
# i a usar eines quan cal
prompt = hub.pull("hwchase17/react")
print("✅ Prompt ReAct carregat")

# ─── 4. CREAR L'AGENT ────────────────────────────────────────────
agent = create_react_agent(
    llm=llm,
    tools=tools,
    prompt=prompt
)

# ─── 5. EXECUTOR ─────────────────────────────────────────────────
executor = AgentExecutor(
    agent=agent,
    tools=tools,
    verbose=True,               # ← Mostra el raonament intern (IMPRESCINDIBLE per aprendre!)
    max_iterations=6,           # Màxim d'iteracions per evitar bucles infinits
    max_execution_time=30,      # Timeout en segons
    handle_parsing_errors=True, # Gestionar errors de format automàticament
    return_intermediate_steps=True  # Retornar tots els passos intermedis
)

print("✅ Agent creat i llest!")
print("=" * 60)

# ─── 6. EXECUTAR TASQUES ─────────────────────────────────────────
def fer_consulta(pregunta: str) -> dict:
    """Executa una consulta a l'agent i retorna el resultat complet."""
    print(f"\n🤔 PREGUNTA: {pregunta}\n")
    print("-" * 60)

    try:
        result = executor.invoke({"input": pregunta})

        print("\n" + "=" * 60)
        print(f"✅ RESPOSTA FINAL:\n{result['output']}")

        # Mostrar resum dels passos
        steps = result.get("intermediate_steps", [])
        print(f"\n📊 Estadístiques:")
        print(f"   - Iteracions: {len(steps)}")
        print(f"   - Eines usades: {[s[0].tool for s in steps]}")

        return result

    except Exception as e:
        print(f"❌ Error: {e}")
        return {"error": str(e)}

# Preguntes de prova
if __name__ == "__main__":
    preguntes = [
        "Quina és la versió actual de Python i quan es va publicar?",
        "Quins són els 3 LLM open-source més populars ara mateix?",
    ]

    for pregunta in preguntes:
        fer_consulta(pregunta)
        input("\n[Prem ENTER per a la següent pregunta...]")

Executar i Observar

python agent_basic.py

📝 Punt de Reflexió 1.1

Observa la sortida amb verbose=True i respon al teu quadern de pràctiques:

  1. Quants "Thought → Action → Observation" ha necessitat per a cada pregunta?
  2. El model ha justificat correctament PER QUÈ usava l'eina de cerca?
  3. Hi ha algun moment en que el model hagi fet una inferència incorrecta?

💻 Part 2: Agent amb Eines Personalitzades (90 minuts)

Ara crearem eines pròpies usant el decorador @tool:

"""
Pràctica 1 — Part 2: Eines Personalitzades
"""

import math
import json
from datetime import datetime
from langchain.tools import tool
from langchain_openai import ChatOpenAI
from langchain.agents import create_react_agent, AgentExecutor
from langchain import hub
from dotenv import load_dotenv

load_dotenv()

# ─── EINES PERSONALITZADES AMB @tool ─────────────────────────────

@tool
def calculadora(expressio: str) -> str:
    """
    Realitza càlculs matemàtics.

    Accepta expressions com: '25 * 4 + 10', 'sqrt(144)', '2**10', 'pi * 5**2'
    NO usar per a preguntes factuals, NOMÉS per a càlculs numèrics.

    Args:
        expressio: L'expressió matemàtica a calcular

    Returns:
        El resultat del càlcul com a string
    """
    # Context segur: només operadors matemàtics
    context_segur = {
        "sqrt": math.sqrt,
        "pi": math.pi,
        "e": math.e,
        "sin": math.sin,
        "cos": math.cos,
        "log": math.log,
        "log2": math.log2,
        "log10": math.log10,
        "abs": abs,
        "round": round,
        "__builtins__": {}  # Deshabilitar builtins per seguretat
    }

    try:
        resultat = eval(expressio, context_segur)
        return f"El resultat de '{expressio}' és: {resultat:.6g}"
    except ZeroDivisionError:
        return "Error: divisió per zero"
    except Exception as e:
        return f"Error en el càlcul de '{expressio}': {str(e)}"


@tool
def data_hora_actual(zona_horaria: str = "Europe/Madrid") -> str:
    """
    Retorna la data i hora actuals del sistema.

    Usa aquesta eina quan et preguntin per la data actual, l'hora, 
    el dia de la setmana, o quan hagis de calcular quants dies han 
    passat des d'una data.

    Args:
        zona_horaria: Zona horària (per defecte 'Europe/Madrid')

    Returns:
        Data i hora actuals formatejades
    """
    now = datetime.now()
    dies_setmana = ["Dilluns", "Dimarts", "Dimecres", "Dijous", 
                    "Divendres", "Dissabte", "Diumenge"]
    mesos = ["Gener", "Febrer", "Març", "Abril", "Maig", "Juny",
             "Juliol", "Agost", "Setembre", "Octubre", "Novembre", "Desembre"]

    return (
        f"Data i hora actuals ({zona_horaria}):\n"
        f"  - {dies_setmana[now.weekday()]}, "
        f"{now.day} de {mesos[now.month-1]} de {now.year}\n"
        f"  - Hora: {now.strftime('%H:%M:%S')}\n"
        f"  - Dia de l'any: {now.timetuple().tm_yday}\n"
        f"  - Setmana de l'any: {now.isocalendar()[1]}"
    )


@tool
def conversor_unitats(valor: float, de: str, a: str) -> str:
    """
    Converteix entre unitats de mesura comunes.

    Unitats suportades:
    - Temperatura: celsius, fahrenheit, kelvin
    - Distància: km, milles, metres, peus
    - Pes: kg, lliures, grams

    Args:
        valor: El valor numèric a convertir
        de: Unitat d'origen (p.ex. 'celsius')
        a: Unitat de destí (p.ex. 'fahrenheit')

    Returns:
        El valor convertit amb la unitat
    """
    conversions = {
        # Temperatura
        ("celsius", "fahrenheit"): lambda x: x * 9/5 + 32,
        ("fahrenheit", "celsius"): lambda x: (x - 32) * 5/9,
        ("celsius", "kelvin"): lambda x: x + 273.15,
        ("kelvin", "celsius"): lambda x: x - 273.15,
        # Distància
        ("km", "milles"): lambda x: x * 0.621371,
        ("milles", "km"): lambda x: x * 1.60934,
        ("metres", "peus"): lambda x: x * 3.28084,
        ("peus", "metres"): lambda x: x * 0.3048,
        # Pes
        ("kg", "lliures"): lambda x: x * 2.20462,
        ("lliures", "kg"): lambda x: x * 0.453592,
        ("kg", "grams"): lambda x: x * 1000,
        ("grams", "kg"): lambda x: x / 1000,
    }

    key = (de.lower(), a.lower())
    if key in conversions:
        resultat = conversions[key](valor)
        return f"{valor} {de} = {resultat:.4f} {a}"
    else:
        return (f"Conversió de '{de}' a '{a}' no suportada. "
                f"Unitats disponibles: celsius, fahrenheit, kelvin, "
                f"km, milles, metres, peus, kg, lliures, grams")


# ─── CREAR L'AGENT AMB LES NOSTRES EINES ─────────────────────────

from langchain_community.tools import DuckDuckGoSearchResults

llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)

tools = [
    calculadora,
    data_hora_actual,
    conversor_unitats,
    DuckDuckGoSearchResults(name="cerca_web", num_results=3),
]

prompt = hub.pull("hwchase17/react")
agent = create_react_agent(llm, tools, prompt)
executor = AgentExecutor(
    agent=agent, tools=tools,
    verbose=True, max_iterations=8,
    handle_parsing_errors=True
)

# ─── TASQUES DE PROVA ─────────────────────────────────────────────

if __name__ == "__main__":
    tasques_prova = [
        # Usa la calculadora
        "Quant és la superfície d'un cercle amb radi 7.5 metres?",
        # Usa data + calculadora
        "Quants dies han passat desde l'1 de gener d'aquest any fins avui?",
        # Usa el conversor
        "Quants graus Fahrenheit són 100 graus Celsius? I 37 graus?",
        # Combina cerca + calculadora
        "Quin és el PIB d'Espanya i quin percentatge és respecte al PIB mundial?",
    ]

    for i, tasca in enumerate(tasques_prova, 1):
        print(f"\n{'='*60}")
        print(f"📝 TASCA {i}/{len(tasques_prova)}: {tasca}")
        print('='*60)
        result = executor.invoke({"input": tasca})
        print(f"\n✅ RESULTAT: {result['output']}")

        if i < len(tasques_prova):
            input("\n[ENTER per continuar...]\n")

💻 Part 3: Entrega i Documentació (30 minuts)

Estructura d'Entrega

practica1-primer-agent/
├── .env                    ← NO incloure al ZIP d'entrega!
├── .gitignore
├── requirements.txt
├── agent_basic.py          ← Part 1
├── agent_eines.py          ← Part 2
├── nova_eina.py            ← EXERCICI FINAL (veure baix)
├── captures/
│   ├── execucio_part1.png  ← Captura de pantalla de l'execució
│   ├── execucio_part2.png
│   └── execucio_nova.png
└── informe.md              ← Reflexions i respostes als punts de reflexió

Generar requirements.txt

pip freeze > requirements.txt

🏆 Exercici Final: Eina Pròpia (puntuació addicional)

Repte Final

Crea una eina personalitzada que NO sigui una de les que hem vist a classe. Algunes idees:

  • 🌤️ Eina meteorològica (usar wttr.in API, que és gratuïta i no requereix clau)
  • 📊 Eina de generació de gràfics (matplotlib → retornar una imatge en base64)
  • 📝 Eina de lectura de fitxers locals (llegir CSV o TXT)
  • 🔐 Eina de generació de contrasenyes segures
  • 🎨 Eina de conversió de colors (hex ↔ RGB ↔ HSL)

Requisits mínims de la nova eina: 1. Nom descriptiu i docstring completa 2. Gestió d'errors adequada 3. Prova l'eina dins d'un agent amb almenys 3 tasques

# Exemple d'estructura per a l'eina de temps
import requests
from langchain.tools import tool

@tool
def temps_actual(ciutat: str) -> str:
    """
    Obté el temps meteorològic actual d'una ciutat.
    Usa aquesta eina quan et preguntin sobre el temps, temperatura, 
    o condicions meteorològiques d'una ubicació.

    Args:
        ciutat: Nom de la ciutat (p.ex. 'Barcelona', 'Madrid', 'London')

    Returns:
        Informació meteorològica actual de la ciutat
    """
    try:
        # wttr.in és una API de temps gratuïta i sense clau
        url = f"https://wttr.in/{ciutat}?format=j1&lang=ca"
        response = requests.get(url, timeout=10)
        response.raise_for_status()

        data = response.json()
        current = data["current_condition"][0]

        return (
            f"Temps a {ciutat}:\n"
            f"  - Temperatura: {current['temp_C']}°C "
            f"(sensació: {current['FeelsLikeC']}°C)\n"
            f"  - Descripció: {current['weatherDesc'][0]['value']}\n"
            f"  - Humitat: {current['humidity']}%\n"
            f"  - Vent: {current['windspeedKmph']} km/h"
        )
    except requests.exceptions.RequestException as e:
        return f"Error obtenint el temps: {str(e)}"
    except (KeyError, IndexError) as e:
        return f"Ciutat '{ciutat}' no trobada o format de resposta inesperat."

📊 Criteris d'Avaluació

Criteri Pes Indicadors
L'agent s'executa sense errors 20% Cap excepció no controlada
Les eines retornen resultats correctes 25% Càlculs, dates i conversions precises
L'agent usa les eines adequades 20% Tria l'eina correcta per a cada tasca
Qualitat del codi (comentaris, estructura) 15% PEP 8, docstrings, gestió d'errors
Informe de reflexió 10% Resposta als punts de reflexió
Eina pròpia (exercici final) 10% Funcional, documentada, provada

Format d'Entrega

Entregar un ZIP (sense la carpeta .venv i sense el fitxer .env) a la plataforma Moodle del curs, amb el nom: P1_NomCognom_AgentsIA.zip