feat: registrar proyectos con fases, adjuntar documentos y asignar tareas por Telegram
- crear_fase_proyecto: agrega fases/etapas a un proyecto (se puede llamar varias veces seguidas para cargar todas las fases de un proyecto nuevo). - adjuntar_documento_proyecto: guarda el archivo que el usuario acaba de enviar por Telegram como documento del proyecto (mismo mecanismo de adjuntos ya usado para facturas — se generalizó TelegramAttachment para servir a ambos casos). - listar_usuarios + asignado_id en crear_tarea + asignar_tarea: permite asignar tareas a alguien del equipo por Telegram, disparando la misma notificación (Telegram/email/sistema) que ya dispara el dashboard. - El prompt del sistema ahora indica cómo decidir entre adjuntar_factura y adjuntar_documento_proyecto según el contexto del archivo recibido. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
cd0ca6d232
commit
a0b2e9ae0a
@@ -282,6 +282,22 @@ func agentTools() []agentTool {
|
||||
"stack": str("Stack tecnológico, ej: Go + React + PostgreSQL"),
|
||||
"descripcion": str("Descripción adicional del proyecto"),
|
||||
}, []string{"cliente_id", "nombre"})),
|
||||
tool("crear_fase_proyecto", "Agrega una fase/etapa a un proyecto existente (ej: Diseño, Desarrollo, QA, Entrega). Se puede llamar varias veces seguidas para registrar todas las fases de un proyecto de una vez.",
|
||||
obj(map[string]agentToolParam{
|
||||
"proyecto_id": num("ID del proyecto (usa listar_proyectos si no lo sabes)"),
|
||||
"nombre": str("Nombre de la fase"),
|
||||
"descripcion": str("Descripción de la fase"),
|
||||
"estado": agentToolParam{Type: "string", Enum: []string{"pendiente", "en_progreso", "completado"}, Description: "Estado inicial (default pendiente)"},
|
||||
"orden": num("Orden de la fase dentro del proyecto (0, 1, 2...)"),
|
||||
"fecha_estimada": str("Fecha estimada de entrega, formato YYYY-MM-DD"),
|
||||
}, []string{"proyecto_id", "nombre"})),
|
||||
tool("adjuntar_documento_proyecto", "Guarda como documento del proyecto el archivo (PDF/foto) que el usuario acaba de enviar por Telegram en este chat. Solo funciona si hay un archivo adjunto pendiente.",
|
||||
obj(map[string]agentToolParam{
|
||||
"proyecto_id": num("ID del proyecto (usa listar_proyectos si no lo sabes)"),
|
||||
"tipo": agentToolParam{Type: "string", Enum: []string{"contrato", "orden_servicio", "otro"}, Description: "Tipo de documento (default otro)"},
|
||||
"nombre": str("Nombre del documento"),
|
||||
"descripcion": str("Descripción breve"),
|
||||
}, []string{"proyecto_id"})),
|
||||
|
||||
// ── Tickets ──────────────────────────────────────────────────────────
|
||||
tool("listar_tickets", "Lista tickets de soporte de todos los proyectos.",
|
||||
@@ -300,13 +316,22 @@ func agentTools() []agentTool {
|
||||
obj(map[string]agentToolParam{
|
||||
"estado": str("pendiente | en_progreso | completada | cancelada"),
|
||||
}, nil)),
|
||||
tool("crear_tarea", "Crea una nueva tarea.",
|
||||
tool("listar_usuarios", "Lista los usuarios internos del sistema (staff), para poder asignarles tareas.",
|
||||
obj(map[string]agentToolParam{"search": str("Búsqueda por nombre")}, nil)),
|
||||
tool("crear_tarea", "Crea una nueva tarea, opcionalmente asignada a alguien desde el inicio.",
|
||||
obj(map[string]agentToolParam{
|
||||
"titulo": str("Título de la tarea"),
|
||||
"descripcion": str("Descripción"),
|
||||
"estado": agentToolParam{Type: "string", Enum: []string{"pendiente", "en_progreso"}, Description: "Estado inicial"},
|
||||
"prioridad": agentToolParam{Type: "string", Enum: []string{"baja", "media", "alta"}, Description: "Prioridad"},
|
||||
"titulo": str("Título de la tarea"),
|
||||
"descripcion": str("Descripción"),
|
||||
"estado": agentToolParam{Type: "string", Enum: []string{"pendiente", "en_progreso"}, Description: "Estado inicial"},
|
||||
"prioridad": agentToolParam{Type: "string", Enum: []string{"baja", "media", "alta"}, Description: "Prioridad"},
|
||||
"asignado_id": num("ID del usuario responsable (usa listar_usuarios si no lo sabes)"),
|
||||
"fecha_limite": str("Fecha límite, formato YYYY-MM-DD"),
|
||||
}, []string{"titulo"})),
|
||||
tool("asignar_tarea", "Asigna (o reasigna) una tarea existente a un usuario. Notifica al usuario asignado.",
|
||||
obj(map[string]agentToolParam{
|
||||
"id": num("ID de la tarea"),
|
||||
"asignado_id": num("ID del usuario responsable (usa listar_usuarios si no lo sabes)"),
|
||||
}, []string{"id", "asignado_id"})),
|
||||
tool("actualizar_estado_tarea", "Cambia el estado de una tarea.",
|
||||
obj(map[string]agentToolParam{
|
||||
"id": num("ID de la tarea"),
|
||||
@@ -470,7 +495,7 @@ func runTool(chatID int64, name string, a map[string]interface{}) (interface{},
|
||||
return map[string]interface{}{"items": items, "total": total, "page": page}, nil
|
||||
|
||||
case "adjuntar_factura":
|
||||
att, ok := PopFacturaAttachment(chatID)
|
||||
att, ok := PopTelegramAttachment(chatID)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("no hay ningún documento pendiente en este chat; pide al usuario que lo envíe de nuevo")
|
||||
}
|
||||
@@ -805,6 +830,26 @@ func runTool(chatID int64, name string, a map[string]interface{}) (interface{},
|
||||
}
|
||||
return map[string]interface{}{"ok": true, "id": p.ID, "slug": p.Slug}, nil
|
||||
|
||||
case "crear_fase_proyecto":
|
||||
var fecha *time.Time
|
||||
if s := getStr("fecha_estimada"); s != "" {
|
||||
if t, errParse := time.Parse("2006-01-02", s); errParse == nil {
|
||||
fecha = &t
|
||||
}
|
||||
}
|
||||
f, err := CrearFaseProyecto(uint(getInt("proyecto_id", 0)), getStr("nombre"), getStr("descripcion"), getStr("estado"), getInt("orden", 0), fecha)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return map[string]interface{}{"ok": true, "fase_id": f.ID}, nil
|
||||
|
||||
case "adjuntar_documento_proyecto":
|
||||
d, err := AdjuntarDocumentoProyecto(chatID, uint(getInt("proyecto_id", 0)), getStr("tipo"), getStr("nombre"), getStr("descripcion"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return map[string]interface{}{"ok": true, "documento_id": d.ID}, nil
|
||||
|
||||
// ── Tickets ────────────────────────────────────────────────────────────
|
||||
case "listar_tickets":
|
||||
estado := getStr("estado")
|
||||
@@ -849,24 +894,52 @@ func runTool(chatID int64, name string, a map[string]interface{}) (interface{},
|
||||
}
|
||||
return filtered, nil
|
||||
|
||||
case "listar_usuarios":
|
||||
users, _, err := models.AllUsersSistema(50, 0, getStr("search"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
type usuarioItem struct {
|
||||
ID uint `json:"id"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
out := make([]usuarioItem, len(users))
|
||||
for i, u := range users {
|
||||
out[i] = usuarioItem{ID: u.ID, Name: u.Name}
|
||||
}
|
||||
return out, nil
|
||||
|
||||
case "crear_tarea":
|
||||
t := &models.Tarea{
|
||||
Titulo: getStr("titulo"),
|
||||
Descripcion: getStr("descripcion"),
|
||||
Estado: getStr("estado"),
|
||||
Prioridad: getStr("prioridad"),
|
||||
var asignadoID *uint
|
||||
if v := getInt("asignado_id", 0); v > 0 {
|
||||
u := uint(v)
|
||||
asignadoID = &u
|
||||
}
|
||||
if t.Estado == "" {
|
||||
t.Estado = "pendiente"
|
||||
var fechaLimite *time.Time
|
||||
if s := getStr("fecha_limite"); s != "" {
|
||||
if t, errParse := time.Parse("2006-01-02", s); errParse == nil {
|
||||
fechaLimite = &t
|
||||
}
|
||||
}
|
||||
if t.Prioridad == "" {
|
||||
t.Prioridad = "media"
|
||||
}
|
||||
if err := models.CreateTarea(t); err != nil {
|
||||
t, err := CrearTareaAsignada(getStr("titulo"), getStr("descripcion"), getStr("estado"), getStr("prioridad"), asignadoID, fechaLimite)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return map[string]interface{}{"ok": true, "id": t.ID, "titulo": t.Titulo}, nil
|
||||
|
||||
case "asignar_tarea":
|
||||
id := uint(getInt("id", 0))
|
||||
asignadoIDVal := getInt("asignado_id", 0)
|
||||
if id == 0 || asignadoIDVal == 0 {
|
||||
return nil, fmt.Errorf("id y asignado_id requeridos")
|
||||
}
|
||||
asignadoID := uint(asignadoIDVal)
|
||||
t, err := AsignarTarea(id, &asignadoID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return map[string]interface{}{"ok": true, "id": t.ID, "asignado_id": asignadoID}, nil
|
||||
|
||||
case "actualizar_estado_tarea":
|
||||
id := uint(getInt("id", 0))
|
||||
estado := getStr("estado")
|
||||
@@ -1269,7 +1342,12 @@ COMPORTAMIENTO:
|
||||
- Los valores monetarios son en COP (pesos colombianos)
|
||||
- Si una herramienta falla, explica el error y sugiere alternativas
|
||||
- Para acciones en Coolify, primero usa coolify_instancias para saber qué IDs hay disponibles si el usuario no lo especifica
|
||||
- Cuando el mensaje empiece con "[Documento adjunto recibido: ...]", el usuario acaba de enviar un PDF o foto por Telegram y puedes leerlo directamente (está adjunto a este mismo mensaje, no es solo un nombre de archivo). Léelo para identificar cliente, monto y número de factura. Usa listar_clientes para encontrar el cliente_id que mejor coincida con el nombre/empresa que aparece en el documento o en el texto del usuario. Si logras identificar cliente y monto (del documento o de lo que escribió el usuario), llama a adjuntar_factura directamente sin pedir confirmación — solo pregunta si de verdad no hay forma de determinar el cliente o el monto. No uses esta tool si el mensaje no menciona ningún documento adjunto
|
||||
- Cuando el mensaje empiece con "[Documento adjunto recibido: ...]", el usuario acaba de enviar un PDF o foto por Telegram y puedes leerlo directamente (está adjunto a este mismo mensaje, no es solo un nombre de archivo). Decide primero de qué se trata el archivo antes de guardarlo:
|
||||
· Si es una factura/cuenta de cobro → identifica cliente, monto y número, resuelve el cliente_id con listar_clientes, y llama a adjuntar_factura
|
||||
· Si es un documento de un proyecto (contrato, orden de servicio, entregable, etc.) → identifica a qué proyecto corresponde (usa listar_proyectos si hace falta) y llama a adjuntar_documento_proyecto
|
||||
Si logras identificar los datos necesarios (del documento o de lo que escribió el usuario), guarda directamente sin pedir confirmación — solo pregunta si de verdad no hay forma de determinar el cliente/proyecto o falta un dato imprescindible. No uses estas tools si el mensaje no menciona ningún documento adjunto
|
||||
- Para registrar un proyecto nuevo con sus fases (el usuario puede describirlo en texto o mandarlo en un documento), primero usa crear_proyecto y luego llama a crear_fase_proyecto una vez por cada fase que corresponda — no hace falta preguntar confirmación entre cada fase
|
||||
- Para asignar una tarea a alguien, resuelve el nombre con listar_usuarios y usa asignado_id en crear_tarea (si es nueva) o asignar_tarea (si ya existe) — la persona asignada recibe una notificación
|
||||
|
||||
COMANDOS ESPECIALES (el usuario puede escribirlos):
|
||||
- /reset — olvidar el historial de esta conversación
|
||||
@@ -1342,7 +1420,7 @@ Comandos: /reset /instancias /ayuda`, nil
|
||||
// como archivo real (solo Anthropic lo lee de verdad) en vez de que la IA
|
||||
// tenga que adivinar cliente/monto a partir del nombre del archivo.
|
||||
if strings.ToLower(ai.Provider) == "anthropic" {
|
||||
if att, ok := PeekFacturaAttachment(chatID); ok {
|
||||
if att, ok := PeekTelegramAttachment(chatID); ok {
|
||||
if data, err := os.ReadFile(att.Path); err == nil {
|
||||
userMsg.AttachmentBase64 = base64.StdEncoding.EncodeToString(data)
|
||||
userMsg.AttachmentMime = ResolverMimeType(att.MimeType, att.OriginalName)
|
||||
|
||||
Reference in New Issue
Block a user