Metadata-Version: 2.4
Name: pluma-ai-framework
Version: 0.1.0
Summary: 🐓 Un micro-framework para agentes de IA. Escribe código que escribe código.
Author-email: casromur <casromur@gmail.com>
License: MIT
Project-URL: Homepage, https://github.com/casromur/pluma
Project-URL: Repository, https://github.com/casromur/pluma
Project-URL: Documentation, https://github.com/casromur/pluma/blob/main/docs/api.md
Project-URL: Bug Tracker, https://github.com/casromur/pluma/issues
Project-URL: Changelog, https://github.com/casromur/pluma/releases
Keywords: ai,agents,llm,framework,plumifero
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: pydantic>=2.0.0
Requires-Dist: httpx>=0.25.0
Requires-Dist: rich>=13.0.0
Requires-Dist: typer>=0.9.0
Provides-Extra: ollama
Requires-Dist: ollama>=0.1.0; extra == "ollama"
Provides-Extra: openai
Requires-Dist: openai>=1.0.0; extra == "openai"
Provides-Extra: anthropic
Requires-Dist: anthropic>=0.20.0; extra == "anthropic"
Provides-Extra: dev
Requires-Dist: pytest>=7.0.0; extra == "dev"
Requires-Dist: pytest-asyncio>=0.21.0; extra == "dev"
Requires-Dist: ruff>=0.1.0; extra == "dev"
Requires-Dist: mypy>=1.0.0; extra == "dev"
Provides-Extra: all
Requires-Dist: pluma[anthropic,ollama,openai]; extra == "all"
Dynamic: license-file

# 🐓 pluma

> *"Flask para agentes de IA. El micro-framework que no te dice cómo pensar."*

**pluma** es un micro-framework Python para construir agentes de IA con personalidad, memoria y herramientas. Sin boilerplate. Sin magia negra. Solo código que vuela.

```
pip install pluma
```

---

## ¿Por qué pluma?

| | pluma | LangChain | LlamaIndex |
|---|---|---|---|
| Curva de aprendizaje | 15 minutos | días | días |
| Líneas para un agente básico | ~10 | ~50 | ~40 |
| Dependencias | 4 | 100+ | 80+ |
| Filosofía | Flask | Django | Hibernate |
| Multi-proveedor | ✅ | ✅ | ✅ |
| ReAct loop | ✅ | ✅ | ✅ |
| Streaming | ✅ | ✅ | ✅ |

---

## Instalación

```bash
# Solo el framework
pip install pluma

# Con soporte para Ollama (local)
pip install "pluma[ollama]"

# Con soporte para OpenAI
pip install "pluma[openai]"

# Con soporte para Anthropic
pip install "pluma[anthropic]"

# Todo
pip install "pluma[all]"
```

**Requisitos:** Python 3.10+

---

## Quickstart — 5 minutos

### Chat simple

```python
import asyncio
from pluma import Plumifero, Manuscrito
from pluma import ProveedorOllama

async def main():
    agente = Plumifero(
        proveedor=ProveedorOllama(modelo="llama3.2"),
        manuscrito=Manuscrito(
            nombre="Cafecito",
            proposito="Ayudar a debuggear código antes del primer café.",
            personalidad="Paciente, preciso, con olor a café recién hecho.",
        ),
    )

    respuesta = await agente.dialogar("¿Qué es un decorador en Python?")
    print(respuesta)

asyncio.run(main())
```

### Agente con herramientas (ReAct)

```python
import asyncio
from pluma import Plumifero, Manuscrito, ProveedorOllama
from pluma import pluma_buscar_web, pluma_datetime

async def main():
    agente = Plumifero(
        proveedor=ProveedorOllama(modelo="llama3.2"),
        plumas=[pluma_buscar_web, pluma_datetime],
    )

    resultado = await agente.volar(
        "¿Qué día es hoy y qué noticias hay sobre Python?"
    )
    print(resultado.mensaje_final)
    print(f"Pasos: {len(resultado.pasos)} | Tokens: {resultado.tokens_usados}")

asyncio.run(main())
```

### Agentes predefinidos (zero-config)

```python
from pluma import crear_plumita, crear_don_pio, crear_madrugador, crear_poeta
from pluma import ProveedorAnthropic

proveedor = ProveedorAnthropic(modelo="claude-haiku-4-5-20251001")

# Chat conversacional
plumita = crear_plumita(proveedor)

# Revisor de código implacable
don_pio = crear_don_pio(proveedor)

# Automatización con shell + Python + archivos
madrugador = crear_madrugador(proveedor)

# Poeta que escribe sobre código
poeta = crear_poeta(proveedor)
```

---

## Conceptos clave

### 🪶 Pluma — la herramienta

Una `Pluma` es una función que el agente puede invocar. Define su nombre, descripción y firma para que el LLM sepa cuándo y cómo usarla.

```python
from pluma import Pluma

# Desde una función existente
def obtener_temperatura(ciudad: str, unidad: str = "celsius") -> str:
    """Obtiene la temperatura actual de una ciudad."""
    return f"La temperatura en {ciudad} es de 22°{unidad[0].upper()}"

pluma_clima = Pluma.crear(
    nombre="obtener_temperatura",
    descripcion="Obtiene la temperatura actual de una ciudad.",
    funcion=obtener_temperatura,
)

# Con decorador
@Pluma.desde_funcion
def fibonacci(n: int) -> str:
    """Calcula los primeros N números de la secuencia de Fibonacci."""
    a, b, seq = 0, 1, []
    for _ in range(n):
        seq.append(a); a, b = b, a + b
    return str(seq)
```

### 📜 Manuscrito — el alma

El `Manuscrito` define la identidad, propósito y personalidad del agente. Se convierte en el prompt de sistema enviado al LLM.

```python
from pluma import Manuscrito

manuscrito = Manuscrito(
    nombre="DataBird",
    proposito="Analizar datasets y generar insights accionables.",
    personalidad="Riguroso con los números, pero accesible en las explicaciones.",
    tono="técnico pero claro, como un profesor que sabe de lo que habla",
    idioma="español",
    conocimiento_base=["pandas", "numpy", "estadística descriptiva"],
    restricciones=["No inventes datos. Si no puedes calcular algo, dilo."],
)

# API fluida (inmutable — devuelve nuevo Manuscrito)
manuscrito_v2 = (
    manuscrito
    .con_conocimiento("matplotlib", "seaborn")
    .con_restriccion("No uses datos de usuarios sin anonimizar")
    .con_ejemplo(
        usuario="¿Qué es la media?",
        plumifero="La media es la suma de todos los valores dividida entre el número de elementos."
    )
)

# Serialización
manuscrito.guardar("mi_agente.json")
manuscrito_cargado = Manuscrito.cargar("mi_agente.json")
```

### 🫙 Tintero — la memoria

El `Tintero` gestiona el historial de conversación. Cada `Plumifero` tiene su propio Tintero.

```python
from pluma import Tintero

tintero = Tintero(max_mensajes=50)  # mantiene los últimos 50 mensajes

# Escribir mensajes directamente
tintero.escribir_sistema("Eres un asistente útil.")
tintero.escribir_usuario("¿Hola?")
tintero.escribir_plumifero("¡Hola! ¿En qué puedo ayudarte?")

# Inspeccionar
mensajes = tintero.leer_todo()  # list[dict[str, str]]
sistema = tintero.leer_por_rol(RolMensaje.SISTEMA)

# Limpiar
tintero.vaciar()
```

### 🦜 Plumifero — el agente

El `Plumifero` orquesta todos los componentes: Proveedor + Manuscrito + Tintero + Plumas + Vuelo.

```python
from pluma import Plumifero, ProveedorOllama

agente = Plumifero(
    proveedor=ProveedorOllama(modelo="llama3.2"),
    manuscrito=manuscrito,
    plumas=[pluma_clima, pluma_datetime],
    max_pasos=10,       # máximo de pasos ReAct
    nombre="Agente",    # sobrescribe el nombre del manuscrito
)

# Chat conversacional (una llamada al LLM)
respuesta = await agente.dialogar("¿Qué tiempo hace?")

# Con herramientas (ciclo ReAct completo)
resultado = await agente.volar("Dime la temperatura de Madrid ahora")

# Streaming
async for chunk in agente.dictar("Cuéntame algo interesante"):
    print(chunk, end="", flush=True)

# Gestión de herramientas
agente.emplumar(pluma_nueva)    # añadir (encadenable)
agente.desplumar("nombre")      # quitar (encadenable)

# Memoria
agente.historia()               # list[dict] con todos los mensajes
agente.reiniciar()              # borra la memoria (encadenable)

# Aliases poéticos
await agente.pensar(msg)        # = dialogar()
await agente.emprender_el_vuelo(msg)  # = volar()
agente.recordar()               # = historia()
```

---

## Plumas predefinidas

| Nombre LLM | Tipo | Descripción |
|---|---|---|
| `obtener_datetime` | sync | Fecha y hora actual con formato personalizable |
| `leer_archivo` | sync | Lee un archivo de texto en UTF-8 |
| `escribir_archivo` | sync | Escribe/sobreescribe un archivo (crea directorios intermedios) |
| `listar_directorio` | sync | Lista el contenido de un directorio con emojis |
| `ejecutar_shell` | async | Ejecuta comandos de shell y captura la salida |
| `ejecutar_python` | async | Ejecuta código Python en un subproceso aislado |
| `buscar_web` | async | Busca en DuckDuckGo (sin API key) |

```python
from pluma import (
    pluma_datetime, pluma_leer_archivo, pluma_escribir_archivo,
    pluma_listar_directorio, pluma_shell, pluma_python, pluma_buscar_web,
)
```

---

## Agentes predefinidos

### Plumita — asistente conversacional

```python
from pluma import crear_plumita, ProveedorOpenAI

plumita = crear_plumita(ProveedorOpenAI(modelo="gpt-4o-mini"))
respuesta = await plumita.dialogar("¿Cómo funciona async/await?")
```

Herramientas: `buscar_web`, `datetime`, `leer_archivo`

### Don Pío — revisor de código

```python
from pluma import crear_don_pio, ProveedorAnthropic

don_pio = crear_don_pio(ProveedorAnthropic())
resultado = await don_pio.volar(f"Revisa este código:\n\n{codigo}")
```

Herramientas: `leer_archivo`, `listar_directorio`

### El Madrugador — automatización

```python
from pluma import crear_madrugador, ProveedorOllama

madrugador = crear_madrugador(ProveedorOllama())
resultado = await madrugador.volar(
    "Lista todos los .py, cuenta las líneas de cada uno y escribe un informe."
)
```

Herramientas: `shell`, `python`, `leer_archivo`, `escribir_archivo`, `listar_directorio`, `datetime`

### La Pluma Lírica — poeta

```python
from pluma import crear_poeta, ProveedorOpenAI

poeta = crear_poeta(ProveedorOpenAI())
resultado = await poeta.volar("Escribe un poema sobre un bug de producción a las 3am.")
```

---

## Proveedores

### Ollama (local, gratis)

```python
from pluma import ProveedorOllama

# Requiere: pip install "pluma[ollama]" y Ollama corriendo en localhost
proveedor = ProveedorOllama(modelo="llama3.2")
proveedor = ProveedorOllama(modelo="llama3.2", base_url="http://mi-servidor:11434")
```

### OpenAI

```python
from pluma import ProveedorOpenAI

# Requiere: pip install "pluma[openai]" y OPENAI_API_KEY en el entorno
proveedor = ProveedorOpenAI(modelo="gpt-4o-mini")
proveedor = ProveedorOpenAI(modelo="gpt-4o", api_key="sk-...")
```

### Anthropic

```python
from pluma import ProveedorAnthropic

# Requiere: pip install "pluma[anthropic]" y ANTHROPIC_API_KEY en el entorno
proveedor = ProveedorAnthropic(modelo="claude-haiku-4-5-20251001")
proveedor = ProveedorAnthropic(modelo="claude-sonnet-4-6", api_key="sk-ant-...")
```

---

## CLI

```bash
# Chat interactivo con Plumita
pluma conversar

# Chat con proveedor específico
pluma conversar --proveedor openai --modelo gpt-4o-mini

# Chat con herramientas de sistema (shell, Python, archivos)
pluma conversar --con-herramientas

# Don Pío revisa tu código
pluma revisar src/mi_modulo.py
pluma revisar mi_codigo.py --proveedor anthropic

# El Madrugador ejecuta una tarea con herramientas (ReAct)
pluma volar "lista los archivos .py del directorio y cuéntalos"
pluma volar "busca info sobre Python 3.13" --verbose

# Wizard para crear un Manuscrito
pluma crear
pluma crear --guardar mi_agente.json

# Ver todas las plumas predefinidas
pluma lista-plumas
```

---

## Inspeccionar ResultadoVuelo

```python
resultado = await agente.volar("tarea compleja")

resultado.exitoso          # bool
resultado.mensaje_final    # str — la respuesta del agente
resultado.estado           # EstadoVuelo.COMPLETADO | .LIMITE_PASOS | .ERROR
resultado.tokens_usados    # int — tokens consumidos en total
resultado.duracion_segundos  # float — tiempo del vuelo
resultado.error            # str | None — mensaje de error si falló

# Inspeccionar cada paso del ciclo ReAct
for paso in resultado.pasos:
    paso.pensamiento   # str | None — razonamiento interno del agente
    paso.accion        # str | None — nombre de la herramienta usada
    paso.argumentos    # dict — argumentos pasados a la herramienta
    paso.observacion   # str | None — resultado de la herramienta
    paso.respuesta_final  # str | None — respuesta si es el último paso
```

---

## Estructura del proyecto

```
src/pluma/
├── __init__.py          ← API pública
├── tipos.py             ← Tipos base (Mensaje, Paso, ResultadoVuelo...)
├── pluma.py             ← Pluma (herramienta)
├── tintero.py           ← Tintero (memoria)
├── manuscrito.py        ← Manuscrito + manuscritos predefinidos
├── vuelo.py             ← Motor ReAct
├── plumifero.py         ← Plumifero (agente)
├── cli.py               ← CLI con Typer
└── proveedores/
    ├── base.py          ← Clase abstracta Proveedor
    ├── ollama.py
    ├── openai.py
    └── anthropic.py
    predefinidos/
    ├── plumas.py        ← 7 plumas listas para usar
    └── plumiferos.py    ← 4 fábricas de agentes
```

---

## Desarrollo

```bash
git clone https://github.com/casromur/pluma
cd pluma
pip install -e ".[dev,all]"

# Tests
pytest

# Lint
ruff check src/

# Tipos
mypy src/pluma/
```

---

## Ejemplos

Los ejemplos en [`examples/`](examples/) están listos para ejecutar:

```bash
# Chat básico con Plumita
python examples/01_hola_plumita.py

# Don Pío revisa código (crea un archivo temporal y lo analiza)
python examples/02_don_pio_revisor.py

# El Madrugador en acción: ReAct, streaming y herramientas personalizadas
python examples/03_agente_madrugador.py
```

---

## Licencia

MIT © 2024 Sr Claude

---

*"Todo gran escritor necesita una gran pluma. El Plumífero es ambos." — Don Pío, amanecer de 2024*
