Come integrare GPT in TeamSystem senza sostituire il gestionale
Pattern di integrazione lato API per affiancare un LLM al gestionale TeamSystem senza toccare il core: webhook, prompt italiano, sicurezza dati. Codice TypeScript pronto da adattare.
Integrare un LLM con un gestionale italiano come TeamSystem si scontra con un fatto: il gestionale non vi appartiene davvero. È un prodotto chiuso, con API parziali, processi di certificazione interni, e qualunque modifica al suo core rompe la garanzia di supporto e l’aggiornamento automatico. Per questo la strategia che funziona è affiancare il gestionale con un layer AI che lo legge e lo scrive via API, senza toccarne il codice. Vediamo come, con codice TypeScript adattabile.
Cosa otterrete alla fine
Un servizio Node.js che:
- Riceve un evento da TeamSystem (anagrafica cliente nuova, ticket aperto, mail ricevuta) via webhook o polling
- Recupera i dati di contesto necessari via API TeamSystem
- Chiama un LLM (Claude o GPT) con un prompt italiano contestualizzato
- Scrive il risultato come nota, attività o promemoria in TeamSystem
Il gestionale resta intatto. L’agente vive in un servizio separato, con i suoi log, i suoi limiti di rate, le sue credenziali. Se si rompe l’agente, TeamSystem continua a funzionare normalmente.
Prerequisiti
- Node.js 22+ e pnpm/npm
- Accesso amministrativo a TeamSystem (per generare credenziali API)
- Chiave OpenAI o Anthropic (in
.env, mai hardcoded) - Un endpoint pubblico HTTPS per i webhook (in dev: ngrok o tunneling Cloudflare; in prod: Vercel/Fly/proprio server)
Verifica preliminare che fa la differenza: chiedete al referente TeamSystem quali API REST sono attive sulla vostra installazione. Le edizioni Enterprise e Cloud espongono REST API documentate. Le edizioni on-premise più vecchie talvolta hanno solo SOAP o accesso diretto al database. La strategia di integrazione cambia drasticamente.
Step 1: configurare il progetto
mkdir teamsystem-llm-bridge && cd teamsystem-llm-bridge
pnpm init
pnpm add express zod openai dotenv
pnpm add -D typescript tsx @types/express @types/node
npx tsc --init --target es2022 --module nodenext --moduleResolution nodenext
Struttura minima:
teamsystem-llm-bridge/
├── src/
│ ├── index.ts # express server
│ ├── teamsystem.ts # client API TeamSystem
│ ├── llm.ts # client LLM
│ └── handlers/
│ └── ticket-opened.ts
├── .env # credenziali, NON in git
└── package.json
Step 2: client API TeamSystem
Il pattern standard è un client tipizzato con zod per validare le risposte. Esempio per recuperare un ticket:
// src/teamsystem.ts
import { z } from 'zod'
const TICKET_SCHEMA = z.object({
id: z.string(),
oggetto: z.string(),
descrizione: z.string(),
cliente: z.object({
ragioneSociale: z.string(),
piva: z.string().optional(),
}),
prioritaCodice: z.enum(['BASSA', 'MEDIA', 'ALTA', 'URGENTE']),
dataApertura: z.coerce.date(),
})
export type Ticket = z.infer<typeof TICKET_SCHEMA>
const BASE_URL = process.env.TEAMSYSTEM_API_URL!
const TOKEN = process.env.TEAMSYSTEM_API_TOKEN!
export async function getTicket(ticketId: string): Promise<Ticket> {
const res = await fetch(`${BASE_URL}/api/v1/tickets/${ticketId}`, {
headers: { Authorization: `Bearer ${TOKEN}` },
})
if (!res.ok) throw new Error(`TeamSystem getTicket failed: ${res.status}`)
return TICKET_SCHEMA.parse(await res.json())
}
export async function addNoteToTicket(
ticketId: string,
noteBody: string,
): Promise<void> {
const res = await fetch(`${BASE_URL}/api/v1/tickets/${ticketId}/notes`, {
method: 'POST',
headers: {
Authorization: `Bearer ${TOKEN}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ corpo: noteBody, tipo: 'INTERNA' }),
})
if (!res.ok) throw new Error(`TeamSystem addNote failed: ${res.status}`)
}
Punto critico: la validazione zod non è ridondante. Le API gestionali italiane storicamente cambiano campi senza preavviso fra release. Validare in ingresso vi fa rompere il vostro servizio in modo controllato (con log chiaro) invece che propagare dati malformati al LLM.
Step 3: client LLM con prompt italiano nativo
Il singolo errore più frequente nell’integrazione LLM con gestionali italiani è scrivere il prompt in inglese pensando “tanto il modello traduce”. I modelli sono significativamente più precisi quando l’output e il prompt sono nella stessa lingua. Scriviamo nativamente in italiano.
// src/llm.ts
import OpenAI from 'openai'
const client = new OpenAI({ apiKey: process.env.OPENAI_API_KEY })
export async function suggestTicketResponse(
ticketDescription: string,
clientName: string,
): Promise<string> {
const completion = await client.chat.completions.create({
model: 'gpt-4o-mini',
temperature: 0.3,
max_tokens: 400,
messages: [
{
role: 'system',
content: `Sei un assistente che supporta operatori italiani del customer service.
Il tuo compito: dato il testo di un ticket aperto da un cliente, proponi una bozza di risposta in italiano formale, professionale, concisa (max 8 righe).
Regole vincolanti:
- Italiano corretto, senza anglicismi superflui
- Mai promesse di tempi o costi specifici (decide l'operatore)
- Se la richiesta non è chiara, suggerisci 1-2 domande di chiarimento invece di una risposta
- Apri con "Gentile [nome cliente]" e chiudi con "Cordiali saluti"
- Non aggiungere firme o nominativi del nostro team`,
},
{
role: 'user',
content: `Cliente: ${clientName}\n\nTesto del ticket:\n${ticketDescription}`,
},
],
})
return completion.choices[0]?.message?.content?.trim() ?? ''
}
Tre dettagli che valgono l’inchiostro:
temperature: 0.3per output deterministico professionale. Più alto introduce variabilità non gradita in contesto business.max_tokens: 400come guardrail di costo. Sopra è raro che serva; sotto rischia di tagliare risposte.- System prompt in italiano nativo, con regole vincolanti esplicite. Mai dire “in italian style” o “professionally” in inglese: scrivete le regole nella lingua che volete vedere in output.
Step 4: webhook handler
// src/handlers/ticket-opened.ts
import { getTicket, addNoteToTicket } from '../teamsystem'
import { suggestTicketResponse } from '../llm'
export async function handleTicketOpened(ticketId: string): Promise<void> {
const ticket = await getTicket(ticketId)
// Filtri precoci: non sprecare token su ticket non rilevanti
if (ticket.prioritaCodice === 'URGENTE') {
// Urgenti vanno gestiti da operatore senza intermediazione AI
return
}
if (ticket.descrizione.length < 30) {
// Descrizione troppo corta: probabile spam o mistype
return
}
const suggestion = await suggestTicketResponse(
ticket.descrizione,
ticket.cliente.ragioneSociale,
)
await addNoteToTicket(
ticket.id,
`[Suggerimento AI generato il ${new Date().toISOString()}]\n\n${suggestion}\n\n---\nQuesto è un suggerimento. Verificare e modificare prima di inviare al cliente.`,
)
}
Notare il “filtro precoce”: non chiamate il LLM su ogni ticket. Le regole di esclusione esplicite (priorità urgenti, descrizioni troppo brevi, clienti specifici sensibili) riducono il costo del 30-50% e migliorano il segnale-rumore delle suggestion.
Step 5: server Express con verifica firma webhook
// src/index.ts
import express from 'express'
import crypto from 'node:crypto'
import { z } from 'zod'
import { handleTicketOpened } from './handlers/ticket-opened'
const app = express()
app.use(express.json({ verify: storeRawBody }))
const WEBHOOK_SECRET = process.env.TEAMSYSTEM_WEBHOOK_SECRET!
function storeRawBody(
req: express.Request,
_res: express.Response,
buf: Buffer,
): void {
;(req as express.Request & { rawBody?: Buffer }).rawBody = buf
}
function verifySignature(req: express.Request): boolean {
const signature = req.header('X-TeamSystem-Signature')
if (!signature) return false
const rawBody = (req as express.Request & { rawBody?: Buffer }).rawBody
if (!rawBody) return false
const expected = crypto
.createHmac('sha256', WEBHOOK_SECRET)
.update(rawBody)
.digest('hex')
return crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected))
}
const WEBHOOK_PAYLOAD = z.object({
evento: z.literal('ticket.aperto'),
ticketId: z.string(),
})
app.post('/webhook/teamsystem', async (req, res) => {
if (!verifySignature(req)) {
res.status(401).json({ error: 'firma non valida' })
return
}
const parsed = WEBHOOK_PAYLOAD.safeParse(req.body)
if (!parsed.success) {
res.status(400).json({ error: 'payload non valido' })
return
}
res.status(202).json({ status: 'in-elaborazione' })
handleTicketOpened(parsed.data.ticketId).catch((err) => {
console.error('handleTicketOpened error:', err)
})
})
const PORT = Number(process.env.PORT ?? 3000)
app.listen(PORT, () => console.log(`bridge in ascolto su :${PORT}`))
Tre cose importanti del server:
- Verifica firma HMAC: senza, chiunque conosca l’URL pubblico può iniettare ticket falsi e bruciare i vostri token LLM. Sempre obbligatoria.
- Risposta
202immediata + processing in background: i webhook hanno timeout (TeamSystem tipicamente 10-15 secondi). Una chiamata LLM ne può richiedere 5-20. Rispondete subito che avete preso in carico, processate dopo. timingSafeEqualper il confronto firma: prevenire timing attack sulle credenziali. Banale da fare giusto, banale da fare male.
Trappole comuni
1. Polling invece di webhook se non li avete. Edizioni TeamSystem on-premise vecchie non hanno webhook. La tentazione è polling ogni 60 secondi su tutti i ticket aperti. NON fatelo: brucia rate limit e tempo. Polling sensato: query con filtro dataApertura > ultimo_check, ogni 2-5 minuti, con tracking del cursore.
2. Output LLM piazzato direttamente al cliente. La bozza generata va sempre come nota interna o draft, non come risposta inviata al cliente. L’operatore umano deve confermare. Anche se la qualità è alta, il rischio di hallucination su nomi/date/prezzi specifici di TeamSystem è reale e va mitigato con review umana.
3. Prompt che non specifica chi NON è il modello. Aggiungete sempre nel system prompt: “Non sei un avvocato, non sei un fiscalista, non dare consigli specifici di compliance”. I clienti italiani fanno domande di natura legale/fiscale a chiunque risponda, anche al ticket di supporto IT. Mettete un guardrail esplicito.
4. Costi LLM non monitorati. Senza budget alert sui token, un’integrazione mal configurata può consumare 200-500 euro al giorno in pochi minuti se va in loop. Impostate budget alert via OpenAI/Anthropic console, e logging dei token consumati per richiesta.
5. Dati cliente in chiaro nei log LLM. I provider LLM loggano le richieste (con varie policy di retention). Se passate dati personali o sensibili, abilitate il zero-retention agreement (Anthropic e OpenAI lo offrono per clienti enterprise). Per GDPR: l’esposizione di dati personali al provider è un trasferimento extra-UE per OpenAI/Anthropic e va valutato da DPO.
Variazioni dell’approccio
Variante A: sintesi note storiche cliente. Stesso pattern, ma su evento “cliente aperto”/“cliente visualizzato”: il bridge legge tutte le note storiche del cliente e produce un riepilogo in 5-8 righe. Utile per operatori che ereditano clienti gestiti da colleghi.
Variante B: classificazione automatica priorità. L’agente legge il ticket appena aperto e propone una priorità basata sul contenuto (parole chiave, urgenza percepita, valore cliente storico). Setta solo se la confidenza è alta, altrimenti lascia all’operatore.
Variante C: generazione bozza email follow-up. Su ticket chiusi da N giorni, l’agente genera una bozza di follow-up personalizzata. L’operatore decide se inviare. Tipicamente migliora la customer retention del 5-15% nei contesti B2B.
Variante D: estrazione informazioni da PDF allegati. I ticket spesso hanno PDF allegati (ordini, fatture, comunicazioni). L’agente li parsifica via vision API del LLM ed estrae i campi rilevanti per popolare in automatico i campi strutturati di TeamSystem.
Limitazioni di questo approccio
1. Dipendenza dalle API TeamSystem. Se l’edizione che usate non espone l’API che vi serve (es. scrittura su tabelle custom), la workaround via accesso DB diretto è possibile ma fragile (rompe a ogni update TeamSystem).
2. Latenza percepita. Tra ricezione webhook e nota scritta in TeamSystem passano tipicamente 8-20 secondi (chiamata API + chiamata LLM + scrittura). Per molti use case è accettabile. Per use case sincroni (utente che aspetta) è troppo: serve approccio diverso (streaming risposta direttamente in UI).
3. Costi sotto carico. Sopra le 500 chiamate LLM al giorno i costi crescono in modo significativo. A 5.000/giorno il modello gpt-4o-mini costa circa 5-10 euro al giorno solo di token. Modelli più potenti (gpt-4o, Claude Sonnet) sono 3-5x. Pianificate il budget realisticamente.
4. Non sostituisce la conoscenza di dominio. L’agente può rispondere bene su FAQ standard. Su domande che richiedono conoscenza del contratto specifico del cliente, della storia operativa, di eccezioni note, il LLM da solo non ce la fa. Serve un layer RAG (vector store con knowledge base aziendale) che è un progetto a parte.
FAQ
Quanto costa questa integrazione su base mensile?
Per uno scenario tipico (PMI italiana, 500-1.500 ticket al mese passati dall’agente), il costo LLM si attesta su 30-120 euro al mese con gpt-4o-mini, 100-400 euro al mese con Claude Sonnet o gpt-4o. Aggiungete 30-80 euro al mese di hosting del bridge (Fly.io o Vercel) e monitoring base.
TeamSystem permette di scrivere su tutti i campi via API?
Dipende dall’edizione. Le edizioni Enterprise e Cloud espongono REST API per la maggior parte degli oggetti standard (clienti, ticket, attività, note, allegati). Campi custom aziendali e moduli verticali (gestione cantieri, fashion, ecc.) richiedono spesso accesso a endpoint specifici o autorizzazioni aggiuntive. Verificare con il proprio referente TeamSystem prima di iniziare.
Si possono usare LLM open-source self-hosted invece di GPT/Claude?
Sì, e per dati molto sensibili può essere obbligatorio per compliance. Modelli come Llama 3.3 70B o Mistral Large via Ollama su GPU NVIDIA H100/L40S danno qualità accettabile per molti use case. Costo iniziale 5-15k euro per la GPU o 1-3 euro/ora per istanze cloud GPU. Conviene se i volumi sono alti e il costo dei token cloud supera 500 euro al mese, o se ci sono vincoli regolatori sul data residency.
Come si gestiscono dati personali sensibili (es. clienti in ambito sanitario)?
Tre livelli da considerare: (1) firma di un zero-retention agreement con OpenAI/Anthropic per evitare retention nei log del provider; (2) DPA (Data Processing Agreement) sotto GDPR che disciplina ruoli e trasferimento extra-UE; (3) per dati strettamente sensibili (sanitari ex art. 9 GDPR), valutare LLM self-hosted su infrastruttura italiana/europea per evitare trasferimento extra-UE. La scelta dipende dal vostro DPO.
Come si testa che il prompt funzioni davvero?
Test set manuale di 20-50 ticket reali (anonimizzati), con risposta umana di riferimento. Il prompt si itera finché la qualità è accettabile su almeno l’85% dei casi. Strumenti di eval come Promptfoo o LangSmith automatizzano questa fase quando il volume cresce.
Conclusione
Il pattern di integrazione GPT con TeamSystem (e con la gran parte dei gestionali italiani che espongono API REST decenti) è ripetibile: webhook in entrata, recupero dati di contesto, chiamata LLM con prompt italiano nativo, scrittura come nota o draft, review umana. Il valore reale arriva quando si itera sul prompt sui propri ticket reali, non sul prompt copiato da blog post (incluso questo).
Se state valutando un’integrazione AI sul vostro TeamSystem o gestionale equivalente, parliamone. Possiamo partire da un POC di 2-3 settimane su un singolo flusso, per validare il fit tecnico prima di committarsi a un progetto pieno.
Per approfondire: la pagina pilastro agenti AI, la pagina dedicata all’integrazione AI nel gestionale, e la pagina agente AI customer service dove questo pattern viene usato in scenari di assistenza clienti.