Lo esencial
El cliente corporativo vive en Microsoft. Teams en vez de Slack, SharePoint en vez de Google Drive, Outlook en vez de Gmail, Power Automate en vez de Zapier. Si quieres vender IA a empresas grandes, aprendes el idioma de Microsoft. No es más difícil que lo de siempre, solo es otro dialecto.
Conceptos clave
- Microsoft Graph API: el punto de entrada único a todos los datos de Microsoft 365
- Webhooks de Teams (app Workflows): Claude manda notificaciones a Teams sin bot
- Integración con Office: Word, Excel y PowerPoint mediante bibliotecas de Python
- Power Platform: Power Automate + Claude sin código
- Azure y Microsoft Foundry: infraestructura para un Claude corporativo
- Microsoft 365 Agents SDK (antes Bot Framework): un bot completo en Teams
Teoría
La arquitectura de Microsoft 365
Ecosistema Microsoft 365
├── Comunicación: Teams, Outlook
├── Documentos: Word, Excel, PowerPoint, OneNote
├── Almacenamiento: SharePoint, OneDrive
├── Automatización: Power Automate, Power Apps
├── Datos: Power BI, Dataverse
└── Infraestructura: Azure, Active DirectoryMicrosoft Graph API es una sola API que da acceso a todo. Un token, un punto de entrada.
https://graph.microsoft.com/v1.0/
├── /me → datos del usuario actual
├── /users/{id}/messages → correos de Outlook
├── /teams/{id}/channels → canales de Teams
├── /sites/{id}/drives → archivos de SharePoint
└── /me/calendar/events → eventos del calendarioMicrosoft Teams + Claude
Opción 1: Incoming Webhook (la más simple)
Un webhook es un enlace. Mandas una solicitud POST a ese enlace y el mensaje aparece en el canal de Teams. Sin bot y sin registrar ninguna aplicación.
⚠️ Microsoft está retirando los webhooks viejos de Office 365 Connectors, y pronto va a bloquear la creación de nuevos. Ahora el webhook se hace con la app Workflows de Teams, que está construida sobre Power Automate.
Obtener el webhook:
- Teams → Canal →
...→ Workflows - Elige la plantilla "Send webhook alerts to a channel", configura los parámetros y presiona Save
- Copia el enlace del webhook (los nombres de los menús de Teams cambian: los pasos actuales están en la documentación de Microsoft Learn)
Límites: el mensaje no puede pasar de 28 KB, y si hay más de cuatro solicitudes por segundo, Teams frena la conexión. El workflow está ligado a su dueño: si esa persona deja la organización, el webhook se queda sin dueño, así que agrega coautores.
Envío básico:
import os
import requests
def notify_teams(webhook_url: str, message: str):
"""Mensaje de texto simple a Teams."""
payload = {"text": message}
response = requests.post(webhook_url, json=payload)
return response.status_code in (200, 202)
# Ejemplo
notify_teams(
webhook_url=os.environ["TEAMS_WEBHOOK_URL"],
message="✅ El reporte semanal está listo. Claude analizó 1,247 transacciones."
)Tarjetas adaptativas (formato enriquecido):
El webhook de Workflows acepta Adaptive Cards. El formato viejo MessageCard con botones no lo muestra, así que los botones se hacen con Action.OpenUrl. Revisa cómo se ve la tarjeta en tu canal.
import os
import json
import requests
import anthropic
from datetime import datetime
def notify_teams_rich(webhook_url: str, title: str, facts: dict, actions: list = None):
"""
Tarjeta adaptativa con tabla de datos y botones.
facts = {"Clave": "Valor", ...}
actions = [{"title": "Abrir", "url": "https://..."}]
"""
card = {
"$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
"type": "AdaptiveCard",
"version": "1.2",
"body": [
{"type": "TextBlock", "text": title, "weight": "Bolder", "size": "Medium", "wrap": True},
{"type": "TextBlock", "text": f"Generado: {datetime.now().strftime('%d.%m.%Y %H:%M')}",
"isSubtle": True, "wrap": True},
{"type": "FactSet", "facts": [{"title": k, "value": v} for k, v in facts.items()]}
]
}
if actions:
card["actions"] = [
{"type": "Action.OpenUrl", "title": a["title"], "url": a["url"]} for a in actions
]
payload = {
"type": "message",
"attachments": [{
"contentType": "application/vnd.microsoft.card.adaptive",
"content": card
}]
}
requests.post(webhook_url, json=payload)
# Claude analiza los datos → los manda a Teams
def weekly_sales_report():
data = get_weekly_sales()
client = anthropic.Anthropic()
analysis = "".join(b.text for b in client.messages.create(
model="claude-sonnet-5-5",
max_tokens=300,
messages=[{
"role": "user",
"content": f"Analiza en breve: ventas {data['total']}, clientes nuevos {data['new_clients']}, producto más vendido: {data['top_product']}"
}]
).content if b.type == "text")
notify_teams_rich(
webhook_url=os.environ["TEAMS_WEBHOOK_URL"],
title="📊 Reporte semanal de ventas",
facts={
"Ingresos": f"${data['total']:,}",
"Clientes nuevos": str(data['new_clients']),
"Producto top": data['top_product'],
"Análisis de IA": analysis[:200]
},
actions=[{"title": "Reporte completo", "url": "https://dashboard.company.com"}]
)Opción 2: un bot de IA en Teams (completo)
Un bot de verdad que responde mensajes en Teams. Microsoft está reemplazando el viejo Bot Framework SDK por Microsoft 365 Agents SDK: hay una guía de migración, y lo sensato es empezar los bots nuevos directo en él (también existe Teams SDK). Abajo, una versión con Agents SDK para Python.
# bot.py — bot para Teams con Microsoft 365 Agents SDK (requiere Python 3.10+)
# pip install microsoft-agents-hosting-core microsoft-agents-hosting-aiohttp \
# microsoft-agents-hosting-teams microsoft-agents-authentication-msal anthropic
#
# AGENT_APP (AgentApplication) se crea según la documentación de Agents SDK:
# CloudAdapter, MsalAuth.from_environment(), almacenamiento de estado.
from microsoft_agents.hosting.core import AgentApplication, TurnState, TurnContext
import anthropic
claude = anthropic.AsyncAnthropic()
conversations = {} # Historial por conversation_id (en producción: una base de datos o Redis)
@AGENT_APP.activity("message")
async def on_message(context: TurnContext, _state: TurnState):
user_message = context.activity.text
conversation_id = context.activity.conversation.id
# Cargar el historial de la conversación
history = conversations.get(conversation_id, [])
history.append({"role": "user", "content": user_message})
# Claude responde
response = await claude.messages.create(
model="claude-sonnet-5-5",
max_tokens=1000,
system="Eres el asistente de IA corporativo de la empresa. Responde breve y al grano.",
messages=history
)
reply = "".join(b.text for b in response.content if b.type == "text")
history.append({"role": "assistant", "content": reply})
# Guardar el historial (solo los últimos 20 mensajes)
conversations[conversation_id] = history[-20:]
await context.send_activity(reply)Registrar el bot en Azure:
- portal.azure.com → Bot Services → Create
- Elige "Azure Bot"
- Obtén el App ID y el App Password
- Configura el Teams Channel
- Despliégalo con Azure App Service
Los nombres de los menús del portal de Azure cambian; los pasos actuales están en la documentación de Microsoft 365 Agents SDK.
Microsoft Office + Claude
Word: generar documentos
import json
from datetime import datetime
from docx import Document
from docx.shared import Pt, RGBColor
from docx.enum.text import WD_ALIGN_PARAGRAPH
import anthropic
def generate_word_report(topic: str, data: dict, output_path: str):
"""Claude escribe el contenido → python-docx crea el documento."""
client = anthropic.Anthropic()
# Claude genera contenido estructurado
content_response = "".join(b.text for b in client.messages.create(
model="claude-sonnet-5-5",
max_tokens=3000,
messages=[{
"role": "user",
"content": f"""
Escribe un reporte de negocios sobre el tema: {topic}
Datos que debe incluir:
{json.dumps(data, ensure_ascii=False, indent=2)}
Formato (estricto):
TÍTULO: [nombre]
RESUMEN: [3-4 oraciones]
SECCIÓN1: [nombre]
[texto de la sección]
SECCIÓN2: [nombre]
[texto de la sección]
CONCLUSIONES: [2-3 puntos]
"""
}]
).content if b.type == "text")
# Lo procesamos y creamos el documento de Word
doc = Document()
# Estilos
style = doc.styles['Normal']
style.font.name = 'Arial'
style.font.size = Pt(11)
lines = content_response.split('\n')
for line in lines:
if line.startswith('TÍTULO:'):
title = doc.add_heading(line.replace('TÍTULO:', '').strip(), 0)
title.alignment = WD_ALIGN_PARAGRAPH.CENTER
elif line.startswith('RESUMEN:'):
p = doc.add_paragraph()
run = p.add_run(line.replace('RESUMEN:', '').strip())
run.italic = True
p.paragraph_format.space_after = Pt(12)
elif line.startswith('SECCIÓN'):
section_title = line.split(':', 1)[1].strip() if ':' in line else line
doc.add_heading(section_title, level=1)
elif line.startswith('CONCLUSIONES:'):
doc.add_heading('Conclusiones', level=1)
elif line.strip() and not any(line.startswith(k) for k in ['TÍTULO', 'RESUMEN', 'SECCIÓN', 'CONCLUSIONES']):
doc.add_paragraph(line.strip())
# Agregar fecha y firma
doc.add_page_break()
doc.add_paragraph(f"Generado: {datetime.now().strftime('%d.%m.%Y')}")
doc.add_paragraph("Preparado con Claude AI")
doc.save(output_path)
return output_path
# Uso
generate_word_report(
topic="Reporte trimestral de ventas",
data={"ingresos": "100 000", "crecimiento": "8%", "regiones_top": ["Norte", "Sur"]}, # datos de ejemplo
output_path="sales_report.docx"
)Excel: análisis de datos con Claude
import json
import anthropic
import openpyxl
from openpyxl.styles import Font, PatternFill, Alignment
from openpyxl.chart import BarChart, Reference
def analyze_excel_with_claude(excel_path: str) -> str:
"""Claude analiza los datos de un Excel y devuelve insights."""
wb = openpyxl.load_workbook(excel_path)
ws = wb.active
# Extraer los datos como texto
data_rows = []
headers = [cell.value for cell in ws[1]]
for row in ws.iter_rows(min_row=2, values_only=True):
if any(cell is not None for cell in row):
data_rows.append(dict(zip(headers, row)))
# Las primeras 50 filas para el análisis
sample = data_rows[:50]
client = anthropic.Anthropic()
analysis = "".join(b.text for b in client.messages.create(
model="claude-sonnet-5-5",
max_tokens=1500,
messages=[{
"role": "user",
"content": f"""
Analiza los datos de este Excel:
Columnas: {headers}
Total de filas: {len(data_rows)}
Primeros registros: {json.dumps(sample[:10], ensure_ascii=False, default=str)}
Dame:
1. Una descripción breve de qué hay en la tabla
2. Observaciones clave (3-5 puntos)
3. Anomalías, si las hay
4. Recomendaciones
"""
}]
).content if b.type == "text")
return analysis
def generate_excel_report(data: list, output_path: str, title: str = "AI Report"):
"""Crear un reporte de Excel bien presentado con datos y gráfica."""
wb = openpyxl.Workbook()
ws = wb.active
ws.title = "Datos"
# Título
ws.merge_cells('A1:E1')
title_cell = ws['A1']
title_cell.value = title
title_cell.font = Font(bold=True, size=14, color="FFFFFF")
title_cell.fill = PatternFill(start_color="0076D7", fill_type="solid")
title_cell.alignment = Alignment(horizontal='center')
# Encabezados de columnas
if data:
headers = list(data[0].keys())
for col, header in enumerate(headers, 1):
cell = ws.cell(row=2, column=col, value=header)
cell.font = Font(bold=True)
cell.fill = PatternFill(start_color="E0E7FF", fill_type="solid")
# Datos
for row_idx, row_data in enumerate(data, 3):
for col_idx, value in enumerate(row_data.values(), 1):
ws.cell(row=row_idx, column=col_idx, value=value)
wb.save(output_path)
return output_pathPowerPoint: presentaciones automáticas
from datetime import datetime
from pptx import Presentation
from pptx.util import Inches, Pt
from pptx.dml.color import RGBColor
import anthropic
def generate_pptx_from_claude(topic: str, key_points: list, output_path: str):
"""Claude crea el contenido → python-pptx arma la presentación."""
client = anthropic.Anthropic()
# Generamos las diapositivas
slides_content = "".join(b.text for b in client.messages.create(
model="claude-sonnet-5-5",
max_tokens=2000,
messages=[{
"role": "user",
"content": f"""
Crea la estructura de una presentación de 5 diapositivas sobre el tema: {topic}
Ideas clave: {', '.join(key_points)}
Formato para cada diapositiva:
DIAPOSITIVA N:
TÍTULO: [nombre]
PUNTOS:
- [punto 1]
- [punto 2]
- [punto 3]
NOTAS: [para quien presenta]
---
"""
}]
).content if b.type == "text")
prs = Presentation()
prs.slide_width = Inches(13.33)
prs.slide_height = Inches(7.5)
# Diapositiva de portada
slide = prs.slides.add_slide(prs.slide_layouts[0])
slide.shapes.title.text = topic
slide.placeholders[1].text = f"Preparado: {datetime.now().strftime('%d.%m.%Y')}"
# Procesamos y agregamos las diapositivas
current_slide_data = {}
for line in slides_content.split('\n'):
if line.startswith('DIAPOSITIVA'):
if current_slide_data.get('title'):
_add_content_slide(prs, current_slide_data)
current_slide_data = {'title': '', 'bullets': [], 'notes': ''}
elif line.startswith('TÍTULO:'):
current_slide_data['title'] = line.replace('TÍTULO:', '').strip()
elif line.startswith('- '):
current_slide_data['bullets'].append(line[2:].strip())
elif line.startswith('NOTAS:'):
current_slide_data['notes'] = line.replace('NOTAS:', '').strip()
if current_slide_data.get('title'):
_add_content_slide(prs, current_slide_data)
prs.save(output_path)
return output_path
def _add_content_slide(prs, data):
slide = prs.slides.add_slide(prs.slide_layouts[1])
slide.shapes.title.text = data['title']
tf = slide.placeholders[1].text_frame
tf.text = ''
for bullet in data['bullets']:
p = tf.add_paragraph()
p.text = bullet
p.level = 0Power Platform + Claude
Power Automate: el conector HTTP
En Power Automate, crea un Flow:
Trigger: Manual / Schedule / Email received
↓
Action: HTTP
Method: POST
URL: https://api.anthropic.com/v1/messages
Headers:
x-api-key: [tu clave]
anthropic-version: 2023-06-01
content-type: application/json
Body: {
"model": "claude-haiku-4-5",
"max_tokens": 500,
"messages": [{"role": "user", "content": "@{triggerBody()?['text']}"}]
}
↓
Action: Send email / Post to Teams / Update SharePointEs la integración no-code más simple: Claude sin una sola línea de código. La acción HTTP es uno de los conectores premium de Power Automate, así que necesitas la licencia correspondiente. No pegues la clave de API en el flujo como texto plano: guárdala en un almacén seguro de secretos (por ejemplo, Azure Key Vault).
Power Apps: un conector personalizado
- Power Apps → Custom Connectors → New
- Agrega la API de Anthropic como conector
- Configura las actions: sendMessage, etc.
- Úsalo en cualquier Power App como componente
Microsoft Graph API: el punto único de datos
from msal import ConfidentialClientApplication
import requests
class MSGraphClient:
def __init__(self):
app = ConfidentialClientApplication(
client_id=os.environ["AZURE_CLIENT_ID"],
client_credential=os.environ["AZURE_CLIENT_SECRET"],
authority=f"https://login.microsoftonline.com/{os.environ['AZURE_TENANT_ID']}"
)
token_result = app.acquire_token_for_client(
scopes=["https://graph.microsoft.com/.default"]
)
self.headers = {
"Authorization": f"Bearer {token_result['access_token']}",
"Content-Type": "application/json"
}
self.base = "https://graph.microsoft.com/v1.0"
def get_recent_emails(self, user_id: str, count: int = 10) -> list:
url = f"{self.base}/users/{user_id}/messages"
params = {"$top": count, "$orderby": "receivedDateTime desc"}
r = requests.get(url, headers=self.headers, params=params)
return r.json().get("value", [])
def send_teams_message(self, team_id: str, channel_id: str, message: str):
# Enviar a un canal vía Graph solo funciona con un token delegado (a nombre de un usuario).
# Con un token de aplicación, como en esta clase, Graph devuelve 403: para notificaciones usa el webhook de Workflows.
url = f"{self.base}/teams/{team_id}/channels/{channel_id}/messages"
payload = {"body": {"content": message}}
requests.post(url, headers=self.headers, json=payload)
def get_sharepoint_files(self, site_id: str, folder: str = "root") -> list:
url = f"{self.base}/sites/{site_id}/drive/root:/{folder}:/children"
r = requests.get(url, headers=self.headers)
return r.json().get("value", [])
# Claude procesa datos corporativos
def process_emails_with_claude(user_id: str):
graph = MSGraphClient()
emails = graph.get_recent_emails(user_id, count=20)
email_texts = [
f"De: {e['from']['emailAddress']['address']}\nAsunto: {e['subject']}\n"
f"Fecha: {e['receivedDateTime'][:10]}"
for e in emails
]
client = anthropic.Anthropic()
summary = "".join(b.text for b in client.messages.create(
model="claude-haiku-4-5",
max_tokens=500,
messages=[{
"role": "user",
"content": f"Haz un resumen breve de estos correos y señala los urgentes:\n" + "\n---\n".join(email_texts)
}]
).content if b.type == "text")
return summaryAzure + Claude: infraestructura corporativa
Dos enfoques:
1. La API de Claude directo desde la infraestructura de Azure:
Azure App Service → Claude API (api.anthropic.com)
Azure Functions → Claude API
Azure Container Apps → Claude APITu código corre en Azure y llama directo a Anthropic. Simple y confiable.
2. Claude en Microsoft Foundry (antes Azure AI Foundry):
Si el cliente dice "no podemos sacar datos de Azure", Claude está disponible en Microsoft Foundry. Hay dos opciones de alojamiento: en la infraestructura de Azure ("Hosted on Azure") y en la infraestructura de Anthropic ("Hosted on Anthropic infrastructure"); no todos los modelos están en ambas. Necesitas una suscripción de pago de Azure en un país soportado y suscribirte al modelo a través de Azure Marketplace. Dónde se procesan exactamente los datos y qué regiones están disponibles, revísalo en la documentación de Microsoft Learn y en el contrato: la palabra "Azure" por sí sola no cubre los requisitos de cumplimiento (compliance).
# Claude vía Microsoft Foundry: el mismo SDK de Anthropic, otro cliente y otra dirección
import os
from anthropic import AnthropicFoundry
client = AnthropicFoundry(
api_key=os.environ["AZURE_AI_KEY"],
base_url=os.environ["AZURE_AI_ENDPOINT"], # https://<nombre-del-recurso>.services.ai.azure.com/anthropic
)
response = client.messages.create(
model=os.environ["AZURE_DEPLOYMENT_NAME"], # el nombre del despliegue que elegiste al desplegar
max_tokens=500,
messages=[{"role": "user", "content": "Hola"}]
)
print("".join(b.text for b in response.content if b.type == "text"))En vez de una clave, es mejor usar Microsoft Entra ID (DefaultAzureCredential): mira el ejemplo en la documentación de Microsoft Learn ("Deploy and use Claude models in Microsoft Foundry").
Práctica
Paso 1: Webhook de Teams (15 min)
- Crea un canal de prueba en Teams
- Crea un webhook con la app Workflows y copia el enlace
- Corre el ejemplo básico de la lección
- Verifica que apareció el mensaje
Paso 2: Word con python-docx (20 min)
pip install python-docx anthropicGenera un documento de Word sobre cualquier tema con el código de la lección. Abre el archivo y revisa el formato.
Paso 3: Análisis de Excel (15 min)
pip install openpyxlToma cualquier archivo de Excel con datos. Corre analyze_excel_with_claude(). Mira qué dice Claude de los datos.
Paso 4: Power Automate (opcional, necesitas M365)
Si tienes acceso a Microsoft 365:
- Crea un Flow en Power Automate
- Trigger: manual
- HTTP action → API de Claude (conector premium)
- Manda el resultado a Teams
Paso 5: Ejercicio de práctica
Elige un escenario real:
Opción A: Reporte semanal en Teams. Un script junta datos de cualquier fuente, Claude los analiza y el resultado es una tarjeta en un canal de Teams.
Opción B: Reporte de Word con plantilla. Claude genera el contenido para una tarea concreta y python-docx lo convierte en un documento corporativo.
Opción C: Resumen de correos. Graph API lee el correo, Claude hace el resumen y el resultado llega a Outlook o a Teams.
Herramientas y recursos
- python-docx:
pip install python-docx - openpyxl:
pip install openpyxl - python-pptx:
pip install python-pptx - MSAL (autenticación de Microsoft):
pip install msal - Documentación de Microsoft Graph: learn.microsoft.com/graph
- Documentación de webhooks de Teams: learn.microsoft.com/microsoftteams/platform/webhooks-and-connectors/how-to/add-incoming-webhook
- Microsoft Foundry (antes Azure AI Foundry): ai.azure.com
- Microsoft 365 Agents SDK: learn.microsoft.com/microsoft-365/agents-sdk
- Power Automate: make.powerautomate.com
- Adaptive Cards Designer: adaptivecards.io/designer
Ideas clave
El mercado corporativo vive en Microsoft. Saber integrar Claude con Teams, Office y Power Platform es una habilidad útil para trabajar con clientes corporativos. Empieza por lo más simple: un webhook de Workflows para Teams son unas cuantas decenas de líneas de código. Después, Graph API abre los datos de la organización (dentro de los permisos otorgados). Microsoft Foundry ayuda a responder la pregunta de dónde se alojan los datos para clientes enterprise, pero los requisitos de cumplimiento revísalos en la documentación y en el contrato.
Lecciones relacionadas
La marca se guarda solo en este navegador y no se envía a ningún sitio. Mi progreso