Experto en FastAPI y gestión segura de credenciales multi-tenant para Platform AI Solutions.
NUNCA usar os.getenv("OPENAI_API_KEY") para lógica de agentes.
SIEMPRE usar el sistema de credenciales soberanas:
from app.core.credentials import get_tenant_credential
# Correcto - Credenciales por tenant
api_key = await get_tenant_credential(
tenant_id=tenant_id,
category="openai", # openai, google, smtp, tiendanube, whatsapp_cloud
name="API_KEY"
)
###Categories:
openai: GPT-5.2, gpt-5-minigoogle: Gemini 3 Pro, Gemini 3 Flashsmtp: Email delivery (Modo Agente)tiendanube: E-commerce tokenswhatsapp_cloud: Meta Business APIchatwoot: Chatwoot API (v6.1 uses both CHATWOOT_API_TOKEN and CHATWOOT_BOT_TOKEN)Al procesar mensajes de Chatwoot, SIEMPRE persistir external_chatwoot_id e external_account_id en la tabla chat_conversations. Esto es crítico para que unified_message_delivery pueda responder correctamente.
NO llamar directamente a meta_service o ycloud para envíos desde el Orquestador.
SIEMPRE delegar al whatsapp_service usando el endpoint de Relay:
# Protocolo v6.2.9
relay_payload = {
"to": phone,
"text": text,
"provider": "meta_direct" | "chatwoot" | "ycloud",
"channel_source": "instagram" | "facebook" | "whatsapp",
"tenant_id": tenant_id
}
await client.post("/messages/relay", json=relay_payload)
Beneficio: Manejo automático de Spacing (4s) y Buffer (16s).
user.id es UUIDtenant_id es INTEGER###Solución:
# ❌ MAL - No confiar directamente en current_user.tenant_id
stmt = delete(Agent).where(Agent.tenant_id == current_user.tenant_id)
# ✅ BIEN - Resolver desde tabla users
user_row = await db.pool.fetchrow(
"SELECT tenant_id FROM users WHERE id = $1",
current_user.id
)
real_tenant_int = user_row['tenant_id']
stmt = delete(Agent).where(Agent.tenant_id == real_tenant_int)
TODA query debe filtrar por tenant_id:
# SQLAlchemy 2.0 Async
from sqlalchemy import select
stmt = select(Agent).where(
Agent.id == agent_id,
Agent.tenant_id == tenant_id # CRÍTICO
)
result = await session.execute(stmt)
agent = result.scalar_one_or_none()
new_agent = Agent(
name="Sales Agent",
role="sales",
model_provider="openai",
model_version="gpt-5-mini",
tenant_id=tenant_id, # SIEMPRE incluir
enabled_tools=["search_products", "rag_search"],
channels=["whatsapp", "instagram"]
)
session.add(new_agent)
await session.commit()
rag_documents table)# 1. Metadata en PostgreSQL
doc = RAGDocument(
tenant_id=tenant_id,
filename=filename,
collection="General", # General, ADN Personal, Shadow RAG
file_path=storage_path
)
session.add(doc)
await session.flush() # Obtener ID
# 2. Vectorizar y almacenar en Supabase
chunks = process_document(file_content)
await supabase_vector_store.add_documents(
chunks,
metadata={"tenant_id": tenant_id, "source_id": str(doc.id)}
)
await session.commit()
# 1. Eliminar vectores de Supabase
await supabase.from_("documents").delete().eq(
"metadata->>source_id", str(doc_id)
).execute()
# 2. Eliminar metadata de PostgreSQL
stmt = delete(RAGDocument).where(
RAGDocument.id == doc_id,
RAGDocument.tenant_id == tenant_id
)
await session.execute(stmt)
await session.commit()
from fastapi import APIRouter, Depends, HTTPException
from app.core.deps import verify_admin_token
router = APIRouter()
@router.post("/agents", status_code=201)
async def create_agent(
payload: AgentCreate,
admin_user = Depends(verify_admin_token)
):
# Resolver tenant
tenant_id = await resolve_tenant(admin_user.id)
# Validar credenciales existen
has_creds = await check_credentials(tenant_id, "openai")
if not has_creds:
raise HTTPException(
status_code=400,
detail="OpenAI credentials not configured"
)
# Crear agente
agent = Agent(**payload.dict(), tenant_id=tenant_id)
# ...
return agent
# app/services/tools_registry.py
from langchain.tools import tool
@tool
def search_products(query: str, tenant_id: int) -> dict:
"""Busca productos en Tienda Nube del tenant."""
# Obtener credenciales de Tienda Nube
tn_token = await get_tenant_credential(
tenant_id=tenant_id,
category="tiendanube"
)
# Llamar API
response = requests.get(
f"https://api.tiendanube.com/v1/products/search",
headers={"Authorization": f"Bearer {tn_token}"},
params={"q": query}
)
return response.json()
@tool
async def report_assistance(type: str, score: float, reasoning: str):
"""Registra métricas de ayuda (sales/support) en la DB."""
# Implementado en admin_routes.py (/tools/report_assistance)
# y reflejado en el Dashboard v7.6
pass
tool_entry = Tool(
tenant_id=None, # Global tool
name="search_products",
type="http",
description="Busca productos en catálogo",
prompt_injection="Usa esta tool cuando el usuario pregunte por productos",
config={"timeout": 10}
)
DEFAULT_AGENT_TONE = """
Sos una asesora experta en danza clásica y ballet.
Usá voseo argentino. Sé cálida y profesional.
"""
SYNONYM_DICTIONARY = {
"mallas": "leotardos",
"can can": "medias",
"zapatillas de punta": "puntas"
}
# Orquestador proxy a meta_service
response = await httpx.post(
"http://meta_service:8000/connect",
json={
"code": auth_code,
"redirect_uri": redirect_uri,
"tenant_id": tenant_id
},
headers={"X-Internal-Secret": INTERNAL_SECRET_KEY}
)
# Meta service devuelve Long-Lived Token (60 días)
# Y persiste en credentials automáticamente
# ❌ MAL
raise HTTPException(status_code=400, detail="Error")
# ✅ BIEN
raise HTTPException(
status_code=404,
detail=f"Agent {agent_id} not found or access denied"
)
import logging
logger = logging.getLogger(__name__)
try:
# Operación
pass
except Exception as e:
logger.error(f"Failed to create agent: {str(e)}", exc_info=True)
raise HTTPException(status_code=500, detail="Internal error")
tenant_id?get_tenant_credential para API keys?tenant_id desde tabla users?tools_registry.py?main.py?