Salta el contingut

Pràctica 3 — Agent Multi-Tool

📋 Fitxa de la Pràctica

⏱️ Durada: 4 hores 🎯 RA: RA2 (CA 2.2, CA 2.5, CA 2.6) 📊 Pes: 25% de la nota pràctica 🐍 Python 3.11+ · LangChain 0.3

🎯 Objectius

  • Integrar ≥5 eines diverses en un agent
  • Implementar eines de xarxa rellevants per a ASIX
  • Gestionar errors robustament en totes les eines
  • Avaluar la intel·ligència de selecció d'eines de l'agent

💻 Eines a Implementar

Eina 1: Ping

from langchain.tools import tool
import subprocess
import platform
import re

@tool
def ping_host(hostname: str, count: int = 4) -> str:
    """
    Fa ping a un hostname o IP i retorna estadístiques de connectivitat.
    Útil per verificar si un host és accessible a la xarxa.

    Args:
        hostname: Hostname o adreça IP (p.ex. '8.8.8.8', 'www.google.com')
        count: Nombre de paquets a enviar (1-10, per defecte 4)
    """
    hostname = hostname.strip()
    count = max(1, min(count, 10))  # Limitar entre 1 i 10

    # Validació bàsica
    if not re.match(r'^[a-zA-Z0-9._-]+$', hostname):
        return "Error: hostname invàlid"

    try:
        if platform.system() == "Windows":
            cmd = ["ping", "-n", str(count), hostname]
        else:
            cmd = ["ping", "-c", str(count), "-W", "3", hostname]

        result = subprocess.run(cmd, capture_output=True, text=True, timeout=30)

        if result.returncode == 0:
            # Extreure estadístiques
            output = result.stdout
            return f"Host {hostname} ACCESSIBLE:\n{output[-500:]}"  # Últims 500 chars
        else:
            return f"Host {hostname} NO ACCESSIBLE (timeout o host inexistent)"

    except subprocess.TimeoutExpired:
        return f"Timeout: {hostname} no ha respost en 30 segons"
    except Exception as e:
        return f"Error executant ping: {str(e)}"

Eina 2: Comprovació de Ports

@tool
def comprovar_port(hostname: str, port: int, timeout: float = 5.0) -> str:
    """
    Comprova si un port TCP específic està obert en un host.

    Args:
        hostname: Hostname o IP
        port: Número de port (1-65535)
        timeout: Temps d'espera en segons (1-30)
    """
    import socket

    port = max(1, min(port, 65535))
    timeout = max(1.0, min(timeout, 30.0))

    try:
        sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        sock.settimeout(timeout)
        result = sock.connect_ex((hostname, port))
        sock.close()

        PORTS_CONEGUTS = {
            21: "FTP", 22: "SSH", 23: "Telnet", 25: "SMTP",
            53: "DNS", 80: "HTTP", 110: "POP3", 143: "IMAP",
            443: "HTTPS", 3306: "MySQL", 5432: "PostgreSQL",
            6379: "Redis", 27017: "MongoDB", 8080: "HTTP-Alt"
        }

        servei = PORTS_CONEGUTS.get(port, "Desconegut")

        if result == 0:
            return f"Port {port}/{servei} a {hostname}: OBERT ✅"
        else:
            return f"Port {port}/{servei} a {hostname}: TANCAT ❌"

    except socket.gaierror:
        return f"Error: No s'ha pogut resoldre '{hostname}'"
    except Exception as e:
        return f"Error: {str(e)}"

Eina 3: Informació del Sistema

@tool
def info_sistema(component: str = "tot") -> str:
    """
    Retorna informació sobre el sistema local.

    Args:
        component: Quin component consultar:
            - "cpu": Ús de CPU
            - "memoria": RAM disponible/usada
            - "disc": Espai de disc
            - "xarxa": Interfícies de xarxa
            - "tot": Tot el resum
    """
    import psutil
    import platform

    result = []

    if component in ("cpu", "tot"):
        cpu = psutil.cpu_percent(interval=1)
        result.append(f"CPU: {cpu}% d'ús ({psutil.cpu_count()} nuclis)")

    if component in ("memoria", "tot"):
        mem = psutil.virtual_memory()
        result.append(
            f"RAM: {mem.used/1e9:.1f}GB / {mem.total/1e9:.1f}GB "
            f"({mem.percent}% usat)"
        )

    if component in ("disc", "tot"):
        disc = psutil.disk_usage('/')
        result.append(
            f"Disc (/): {disc.used/1e9:.1f}GB / {disc.total/1e9:.1f}GB "
            f"({disc.percent}% usat)"
        )

    if component in ("xarxa", "tot"):
        interficies = psutil.net_if_addrs()
        for nom, addrs in list(interficies.items())[:3]:  # Limitar a 3
            for addr in addrs:
                if addr.family == 2:  # AF_INET (IPv4)
                    result.append(f"Xarxa {nom}: {addr.address}")

    if component == "tot":
        result.insert(0, f"Sistema: {platform.system()} {platform.release()}")

    return "\n".join(result) if result else f"Component '{component}' no reconegut"

Eina 4: Generació de Configuració

@tool
def generar_config_network(
    tipus: str,
    parametres: str
) -> str:
    """
    Genera plantilles de configuració de xarxa.

    Args:
        tipus: Tipus de configuració: "vlan", "dhcp", "firewall", "nginx", "apache"
        parametres: Paràmetres en format "clau=valor,clau=valor"
            Per a vlan: "id=100,nom=VENDES,ip=192.168.100.0,mascara=24"
            Per a dhcp: "xarxa=192.168.1.0,mascara=24,gateway=192.168.1.1,dns=8.8.8.8"
    """
    # Parsejar paràmetres
    params = {}
    try:
        for item in parametres.split(","):
            k, v = item.strip().split("=")
            params[k.strip()] = v.strip()
    except ValueError:
        return "Error: format de paràmetres incorrecte. Usa 'clau=valor,clau=valor'"

    templates = {
        "vlan": """
! Configuració VLAN {id}{nom}
! Cisco IOS

vlan {id}
 name {nom}
!
interface vlan {id}
 ip address {ip} {mascara_decimal}
 no shutdown
!
""",
        "dhcp": """
# Configuració ISC DHCP Server
# /etc/dhcp/dhcpd.conf

subnet {xarxa} netmask {mascara_decimal} {{
    range {primer_host} {ultim_host};
    option routers {gateway};
    option domain-name-servers {dns};
    option domain-name "local.lan";
    default-lease-time 86400;
    max-lease-time 172800;
}}
""",
    }

    if tipus not in templates:
        return f"Tipus '{tipus}' no suportat. Opcions: {list(templates.keys())}"

    # Calcular màscares i rangs si cal
    try:
        import ipaddress
        if "mascara" in params and "ip" in params:
            net = ipaddress.IPv4Network(f"{params['ip']}/{params['mascara']}", strict=False)
            params["mascara_decimal"] = str(net.netmask)
            hosts = list(net.hosts())
            if hosts:
                params["primer_host"] = str(hosts[0])
                params["ultim_host"] = str(hosts[-1])
    except Exception:
        pass

    try:
        return templates[tipus].format(**params)
    except KeyError as e:
        return f"Falta el paràmetre: {e}"

Eina 5: Cerca Web Tècnica

from langchain_community.tools import DuckDuckGoSearchResults

search_tool = DuckDuckGoSearchResults(
    name="cerca_documentacio_tecnica",
    description=(
        "Cerca documentació tècnica, manuals, RFC, CVEs o solucions "
        "a problemes d'administració de sistemes i xarxes. "
        "Usa termes tècnics precisos per millors resultats."
    ),
    num_results=4
)

💻 Integrar-ho Tot: L'Agent Complet

from langchain.agents import AgentExecutor, create_tool_calling_agent
from langchain_openai import ChatOpenAI
from langchain.prompts import ChatPromptTemplate
from dotenv import load_dotenv

load_dotenv()

# Requereix psutil: pip install psutil
llm = ChatOpenAI(model="gpt-4o", temperature=0)

tools = [
    ping_host,
    comprovar_port,
    info_sistema,
    generar_config_network,
    search_tool,
]

prompt = ChatPromptTemplate.from_messages([
    ("system", """Ets un expert en administració de sistemes i xarxes (ASIX).
    Tens eines per diagnosticar la xarxa, comprovar ports, generar configuracions
    i buscar documentació tècnica. Usa les eines adequades per a cada tasca.
    Respon en català amb detall tècnic."""),
    ("human", "{input}"),
    ("placeholder", "{agent_scratchpad}"),
])

agent = create_tool_calling_agent(llm, tools, prompt)
executor = AgentExecutor(
    agent=agent, tools=tools,
    verbose=True, max_iterations=8,
    handle_parsing_errors=True
)

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

tasques = [
    "Comprova si els serveis HTTP i HTTPS de google.com estan actius",
    "Genera la configuració VLAN per a la VLAN 200 de nom ALUMNES a la xarxa 10.200.0.0/24",
    "Quin percentatge de RAM i CPU té el sistema ara mateix?",
    "Busca la documentació oficial de com configurar SSH sense contrasenya (only key-based)",
]

for t in tasques:
    print(f"\n{'='*60}\n📝 {t}\n{'='*60}")
    r = executor.invoke({"input": t})
    print(f"\n{r['output']}")

📊 Criteris d'Avaluació

Criteri Pes Indicadors
≥5 eines implementades i funcionals 30% Totes sense errors en casos normals
Gestió d'errors robusta 25% IPs inexistents, ports tancats, timeouts
Intel·ligència de selecció d'eines 25% L'agent tria l'eina correcta per a cada tasca
Qualitat del codi 20% Docstrings, comentaris, estructura