- help.js: nuova sezione 'feedback' con 6 sotto-sezioni (come usare FAB, risposta AI, password gate, le mie segnalazioni, worker autonomo, consigli) - i18n.js: 30 chiavi IT/EN per tutto il sistema feedback - AIService::callAPI: system prompt esteso con lista completa moduli NIS2 Agile - AIService::classifyFeedback: system prompt NIS2-aware Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
568 lines
20 KiB
PHP
568 lines
20 KiB
PHP
<?php
|
||
/**
|
||
* NIS2 Agile - AI Service
|
||
*
|
||
* Integrazione con Anthropic Claude API per:
|
||
* - Analisi gap analysis
|
||
* - Suggerimenti rischi
|
||
* - Generazione policy
|
||
* - Classificazione incidenti
|
||
* - Q&A NIS2
|
||
*/
|
||
|
||
class AIService
|
||
{
|
||
private string $apiKey;
|
||
private string $model;
|
||
private int $maxTokens;
|
||
private string $baseUrl = 'https://api.anthropic.com/v1/messages';
|
||
|
||
public function __construct()
|
||
{
|
||
$this->apiKey = ANTHROPIC_API_KEY;
|
||
$this->model = ANTHROPIC_MODEL;
|
||
$this->maxTokens = ANTHROPIC_MAX_TOKENS;
|
||
|
||
if (empty($this->apiKey) || $this->apiKey === 'sk-ant-xxxxx') {
|
||
throw new RuntimeException('ANTHROPIC_API_KEY non configurata');
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Analizza risultati gap analysis e genera raccomandazioni
|
||
*/
|
||
public function analyzeGapAssessment(array $organization, array $responses, float $overallScore): array
|
||
{
|
||
$responseSummary = $this->summarizeResponses($responses);
|
||
|
||
// Anonimizzazione: non inviare nome org né fatturato esatto ad API esterna
|
||
$employeeRange = $this->employeeRange((int)($organization['employee_count'] ?? 0));
|
||
|
||
$prompt = <<<PROMPT
|
||
Sei un esperto consulente di cybersecurity specializzato nella Direttiva NIS2 (EU 2022/2555) e nel D.Lgs. 138/2024 italiano.
|
||
|
||
Analizza i risultati della gap analysis per l'organizzazione seguente e fornisci raccomandazioni dettagliate.
|
||
|
||
## Organizzazione (dati anonimizzati)
|
||
- Settore: {$organization['sector']}
|
||
- Tipo entità NIS2: {$organization['entity_type']}
|
||
- Dimensione: {$employeeRange}
|
||
|
||
## Risultati Assessment (Score: {$overallScore}%)
|
||
|
||
{$responseSummary}
|
||
|
||
## Istruzioni
|
||
Fornisci la tua analisi in formato JSON con questa struttura:
|
||
{
|
||
"executive_summary": "Riepilogo esecutivo (3-4 frasi)",
|
||
"risk_level": "low|medium|high|critical",
|
||
"top_priorities": [
|
||
{
|
||
"area": "Nome area",
|
||
"nis2_article": "21.2.x",
|
||
"current_status": "Breve descrizione stato attuale",
|
||
"recommendation": "Azione raccomandata specifica",
|
||
"effort": "low|medium|high",
|
||
"timeline": "Tempistica suggerita"
|
||
}
|
||
],
|
||
"strengths": ["Punto di forza 1", "Punto di forza 2"],
|
||
"quick_wins": ["Azione rapida 1", "Azione rapida 2"],
|
||
"compliance_roadmap": [
|
||
{"phase": 1, "title": "Titolo fase", "actions": ["Azione 1"], "duration": "X mesi"}
|
||
]
|
||
}
|
||
|
||
Rispondi SOLO con il JSON, senza testo aggiuntivo.
|
||
PROMPT;
|
||
|
||
$response = $this->callAPI($prompt);
|
||
return $this->parseJsonResponse($response);
|
||
}
|
||
|
||
/**
|
||
* Suggerisce rischi basati su settore e asset
|
||
*/
|
||
public function suggestRisks(array $organization, array $assets = []): array
|
||
{
|
||
$assetList = empty($assets) ? 'Non disponibile' : json_encode($assets, JSON_UNESCAPED_UNICODE);
|
||
|
||
$prompt = <<<PROMPT
|
||
Sei un esperto di cybersecurity risk assessment. Genera una lista di rischi cyber per questa organizzazione.
|
||
|
||
## Organizzazione
|
||
- Settore: {$organization['sector']}
|
||
- Tipo entità NIS2: {$organization['entity_type']}
|
||
- Dipendenti: {$organization['employee_count']}
|
||
|
||
## Asset IT/OT
|
||
{$assetList}
|
||
|
||
Fornisci 10 rischi in formato JSON:
|
||
[
|
||
{
|
||
"title": "Titolo rischio",
|
||
"description": "Descrizione dettagliata",
|
||
"category": "cyber|operational|compliance|supply_chain|physical|human",
|
||
"threat_source": "Fonte della minaccia",
|
||
"vulnerability": "Vulnerabilità sfruttata",
|
||
"likelihood": 1-5,
|
||
"impact": 1-5,
|
||
"nis2_article": "21.2.x",
|
||
"suggested_treatment": "mitigate|accept|transfer|avoid",
|
||
"mitigation_actions": ["Azione 1", "Azione 2"]
|
||
}
|
||
]
|
||
|
||
Rispondi SOLO con il JSON array.
|
||
PROMPT;
|
||
|
||
$response = $this->callAPI($prompt);
|
||
return $this->parseJsonResponse($response);
|
||
}
|
||
|
||
/**
|
||
* Genera bozza di policy
|
||
*/
|
||
public function generatePolicy(string $category, array $organization, ?array $assessmentContext = null): array
|
||
{
|
||
$context = $assessmentContext ? json_encode($assessmentContext, JSON_UNESCAPED_UNICODE) : 'Non disponibile';
|
||
|
||
$prompt = <<<PROMPT
|
||
Sei un esperto di information security policy writing. Genera una policy aziendale per la categoria "{$category}" conforme alla Direttiva NIS2.
|
||
|
||
## Organizzazione (dati anonimizzati)
|
||
- Settore: {$organization['sector']}
|
||
- Tipo entità NIS2: {$organization['entity_type']}
|
||
|
||
## Contesto Assessment
|
||
{$context}
|
||
|
||
Genera la policy in formato JSON:
|
||
{
|
||
"title": "Titolo completo della policy",
|
||
"version": "1.0",
|
||
"category": "{$category}",
|
||
"nis2_article": "21.2.x",
|
||
"content": "Contenuto completo della policy in formato Markdown con sezioni: 1. Scopo, 2. Ambito di applicazione, 3. Responsabilità, 4. Definizioni, 5. Policy (regole specifiche), 6. Procedure operative, 7. Controlli, 8. Non conformità, 9. Revisione",
|
||
"review_period_months": 12
|
||
}
|
||
|
||
La policy deve essere in italiano, professionale, specifica per il settore dell'organizzazione, e conforme ai requisiti NIS2.
|
||
Rispondi SOLO con il JSON.
|
||
PROMPT;
|
||
|
||
$response = $this->callAPI($prompt);
|
||
return $this->parseJsonResponse($response);
|
||
}
|
||
|
||
/**
|
||
* Classifica un incidente e suggerisce severity
|
||
*/
|
||
public function classifyIncident(string $title, string $description, array $organization): array
|
||
{
|
||
$prompt = <<<PROMPT
|
||
Sei un analista di incident response. Classifica il seguente incidente di sicurezza secondo i criteri NIS2.
|
||
|
||
## Incidente
|
||
- Titolo: {$title}
|
||
- Descrizione: {$description}
|
||
|
||
## Organizzazione
|
||
- Settore: {$organization['sector']}
|
||
- Tipo entità: {$organization['entity_type']}
|
||
|
||
Rispondi in formato JSON:
|
||
{
|
||
"classification": "cyber_attack|data_breach|system_failure|human_error|natural_disaster|supply_chain|other",
|
||
"severity": "low|medium|high|critical",
|
||
"is_significant": true/false,
|
||
"significance_reason": "Motivo se significativo secondo NIS2",
|
||
"requires_csirt_notification": true/false,
|
||
"suggested_actions": ["Azione immediata 1", "Azione immediata 2"],
|
||
"potential_impact": "Descrizione impatto potenziale",
|
||
"iocs_to_check": ["Indicatore 1", "Indicatore 2"]
|
||
}
|
||
|
||
Rispondi SOLO con il JSON.
|
||
PROMPT;
|
||
|
||
$response = $this->callAPI($prompt);
|
||
return $this->parseJsonResponse($response);
|
||
}
|
||
|
||
/**
|
||
* Chiama Anthropic API
|
||
*/
|
||
private function callAPI(string $prompt, ?string $systemPrompt = null): string
|
||
{
|
||
$system = $systemPrompt ?? <<<SYSTEM
|
||
Sei un esperto consulente di cybersecurity e compliance NIS2, integrato nella piattaforma SaaS "NIS2 Agile".
|
||
|
||
Moduli della piattaforma che conosci:
|
||
- Gap Analysis (80 domande, 10 categorie Art.21)
|
||
- Risk Management (registro rischi ISO 27005, matrice 5×5)
|
||
- Incident Management (flussi Art.23: early warning 24h, notifica 72h, report 30d)
|
||
- Policy Management (generazione e approvazione policy NIS2)
|
||
- Supply Chain (valutazione fornitori, risk scoring)
|
||
- Training (assegnazione corsi, compliance Art.20)
|
||
- Asset Inventory (asset critici, dipendenze)
|
||
- Audit & Report (controlli, evidenze, export CSV)
|
||
- NCR/CAPA (non conformità, azioni correttive)
|
||
- Whistleblowing (segnalazioni anomalie Art.32)
|
||
- Normative Updates (feed NIS2/ACN/DORA con ACK)
|
||
- Segnalazioni Feedback (bug reporting con risoluzione autonoma AI)
|
||
- Services API (endpoint pubblici per integrazioni B2B)
|
||
|
||
Rispondi sempre in italiano, in modo professionale, preciso e conciso.
|
||
Non includere dati identificativi dell'organizzazione nelle risposte.
|
||
SYSTEM;
|
||
|
||
$body = [
|
||
'model' => $this->model,
|
||
'max_tokens' => $this->maxTokens,
|
||
'system' => $system,
|
||
'messages' => [
|
||
['role' => 'user', 'content' => $prompt],
|
||
],
|
||
];
|
||
|
||
$ch = curl_init($this->baseUrl);
|
||
curl_setopt_array($ch, [
|
||
CURLOPT_HTTPHEADER => [
|
||
'Content-Type: application/json',
|
||
'x-api-key: ' . $this->apiKey,
|
||
'anthropic-version: 2023-06-01',
|
||
],
|
||
CURLOPT_POST => true,
|
||
CURLOPT_POSTFIELDS => json_encode($body),
|
||
CURLOPT_RETURNTRANSFER => true,
|
||
CURLOPT_TIMEOUT => 120,
|
||
]);
|
||
|
||
$response = curl_exec($ch);
|
||
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||
$curlError = curl_error($ch);
|
||
curl_close($ch);
|
||
|
||
if ($curlError) {
|
||
throw new RuntimeException('Errore connessione AI: ' . $curlError);
|
||
}
|
||
|
||
if ($httpCode !== 200) {
|
||
$errorData = json_decode($response, true);
|
||
$errorMessage = $errorData['error']['message'] ?? 'Errore API sconosciuto';
|
||
throw new RuntimeException("Errore API AI ({$httpCode}): {$errorMessage}");
|
||
}
|
||
|
||
$data = json_decode($response, true);
|
||
|
||
if (!isset($data['content'][0]['text'])) {
|
||
throw new RuntimeException('Risposta AI non valida');
|
||
}
|
||
|
||
return $data['content'][0]['text'];
|
||
}
|
||
|
||
/**
|
||
* Converte numero dipendenti in range anonimizzato
|
||
*/
|
||
private function employeeRange(int $count): string
|
||
{
|
||
if ($count <= 0) return 'Non specificato';
|
||
if ($count <= 10) return 'Micro impresa (1-10 dipendenti)';
|
||
if ($count <= 50) return 'Piccola impresa (11-50 dipendenti)';
|
||
if ($count <= 250) return 'Media impresa (51-250 dipendenti)';
|
||
if ($count <= 1000) return 'Grande impresa (251-1000 dipendenti)';
|
||
return 'Grande organizzazione (>1000 dipendenti)';
|
||
}
|
||
|
||
/**
|
||
* Riassume le risposte dell'assessment per il prompt AI
|
||
*/
|
||
private function summarizeResponses(array $responses): string
|
||
{
|
||
$byCategory = [];
|
||
foreach ($responses as $r) {
|
||
$cat = $r['category'] ?? 'other';
|
||
if (!isset($byCategory[$cat])) {
|
||
$byCategory[$cat] = ['implemented' => 0, 'partial' => 0, 'not_implemented' => 0, 'na' => 0, 'total' => 0];
|
||
}
|
||
$byCategory[$cat]['total']++;
|
||
match ($r['response_value']) {
|
||
'implemented' => $byCategory[$cat]['implemented']++,
|
||
'partial' => $byCategory[$cat]['partial']++,
|
||
'not_implemented' => $byCategory[$cat]['not_implemented']++,
|
||
'not_applicable' => $byCategory[$cat]['na']++,
|
||
default => null,
|
||
};
|
||
}
|
||
|
||
$summary = '';
|
||
foreach ($byCategory as $cat => $counts) {
|
||
$pct = $counts['total'] > 0
|
||
? round(($counts['implemented'] * 100 + $counts['partial'] * 50) / (($counts['total'] - $counts['na']) * 100) * 100)
|
||
: 0;
|
||
$summary .= "- {$cat}: {$pct}% (implementati: {$counts['implemented']}, parziali: {$counts['partial']}, non implementati: {$counts['not_implemented']})\n";
|
||
}
|
||
|
||
return $summary;
|
||
}
|
||
|
||
/**
|
||
* Parsing robusto della risposta JSON dall'AI
|
||
*/
|
||
private function parseJsonResponse(string $response): array
|
||
{
|
||
// Rimuovi eventuale markdown code blocks
|
||
$response = preg_replace('/^```(?:json)?\s*/m', '', $response);
|
||
$response = preg_replace('/\s*```\s*$/m', '', $response);
|
||
$response = trim($response);
|
||
|
||
$data = json_decode($response, true);
|
||
|
||
if (json_last_error() !== JSON_ERROR_NONE) {
|
||
error_log('[AI] JSON parse error: ' . json_last_error_msg() . ' | Response: ' . substr($response, 0, 500));
|
||
return ['error' => 'Impossibile analizzare la risposta AI', 'raw' => substr($response, 0, 1000)];
|
||
}
|
||
|
||
return $data;
|
||
}
|
||
|
||
/**
|
||
* Classifica una segnalazione feedback/bug per il sistema di ticketing interno
|
||
*
|
||
* @param string $tipo Tipo segnalazione (bug|ux|funzionalita|domanda|altro)
|
||
* @param string $descrizione Testo della segnalazione (non contiene PII org)
|
||
* @return array ['categoria', 'priorita', 'suggerimento', 'risposta_utente']
|
||
*/
|
||
public function classifyFeedback(string $tipo, string $descrizione): array
|
||
{
|
||
$prompt = <<<PROMPT
|
||
Sei un assistente tecnico per una piattaforma SaaS di compliance NIS2.
|
||
Analizza la seguente segnalazione e rispondi SOLO con un oggetto JSON valido.
|
||
|
||
Tipo segnalazione: {$tipo}
|
||
Descrizione: {$descrizione}
|
||
|
||
Rispondi con questo JSON (senza markdown, solo JSON puro):
|
||
{
|
||
"categoria": "una tra: autenticazione, dashboard, rischi, incidenti, policy, assessment, formazione, fornitori, asset, report, impostazioni, performance, interfaccia, documentazione, altro",
|
||
"priorita": "una tra: alta, media, bassa",
|
||
"suggerimento": "suggerimento tecnico breve per il team di sviluppo (max 150 caratteri)",
|
||
"risposta_utente": "risposta leggibile e rassicurante per l'utente finale in italiano (max 200 caratteri)"
|
||
}
|
||
|
||
Criteri priorità:
|
||
- alta: blocca funzionalità critica (compliance, accesso, salvataggio dati)
|
||
- media: degrada l'esperienza ma esiste un workaround
|
||
- bassa: miglioria estetica o funzionalità secondaria
|
||
PROMPT;
|
||
|
||
$ch = curl_init($this->baseUrl);
|
||
curl_setopt_array($ch, [
|
||
CURLOPT_HTTPHEADER => [
|
||
'Content-Type: application/json',
|
||
'x-api-key: ' . $this->apiKey,
|
||
'anthropic-version: 2023-06-01',
|
||
],
|
||
CURLOPT_POST => true,
|
||
CURLOPT_POSTFIELDS => json_encode([
|
||
'model' => $this->model,
|
||
'max_tokens' => 500,
|
||
'system' => 'Sei l\'assistente tecnico di NIS2 Agile, piattaforma SaaS per compliance NIS2. Conosci tutti i moduli: gap analysis, rischi, incidenti, policy, supply chain, formazione, asset, audit, NCR/CAPA, whistleblowing, normative, feedback. Rispondi SEMPRE con JSON puro, senza markdown né testo aggiuntivo.',
|
||
'messages' => [['role' => 'user', 'content' => $prompt]],
|
||
]),
|
||
CURLOPT_RETURNTRANSFER => true,
|
||
CURLOPT_TIMEOUT => 10,
|
||
]);
|
||
|
||
$response = curl_exec($ch);
|
||
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||
$curlError = curl_error($ch);
|
||
curl_close($ch);
|
||
|
||
if ($curlError || $httpCode !== 200) {
|
||
throw new RuntimeException("classifyFeedback AI error [{$httpCode}]: {$curlError}");
|
||
}
|
||
|
||
$data = json_decode($response, true);
|
||
$text = $data['content'][0]['text'] ?? '';
|
||
|
||
$result = $this->parseJsonResponse($text);
|
||
|
||
// Normalizza priorità
|
||
$validP = ['alta', 'media', 'bassa'];
|
||
if (!in_array($result['priorita'] ?? '', $validP)) {
|
||
$result['priorita'] = 'media';
|
||
}
|
||
|
||
return $result;
|
||
}
|
||
|
||
/**
|
||
* Analisi cross-organizzazione per consulenti (L4)
|
||
* Riceve dati già aggregati e anonimizzati dal controller.
|
||
* NON deve mai ricevere nomi org, P.IVA o dati identificativi.
|
||
*/
|
||
public function crossOrgAnalysis(int $orgCount, array $aggregated, string $question): array
|
||
{
|
||
$context = $this->buildCrossOrgContext($aggregated, $orgCount);
|
||
|
||
$system = <<<SYSTEM
|
||
Sei un analista di cybersecurity e compliance NIS2 specializzato nell'analisi comparativa multi-organizzazione.
|
||
Rispondi sempre in italiano, in modo professionale e sintetico.
|
||
|
||
REGOLA FONDAMENTALE DI PRIVACY (non derogabile):
|
||
- Non fare mai riferimento a organizzazioni specifiche o identificabili.
|
||
- Rispondi SOLO con statistiche aggregate e trend generali.
|
||
- Se una risposta richiederebbe identificare una singola organizzazione, rifiuta con: "Dato non disponibile per protezione della privacy".
|
||
- Non inventare dati non presenti nel contesto.
|
||
SYSTEM;
|
||
|
||
$prompt = <<<PROMPT
|
||
## Portfolio analizzato: {$orgCount} organizzazioni (dati aggregati e anonimizzati)
|
||
|
||
{$context}
|
||
|
||
## Domanda del consulente
|
||
{$question}
|
||
|
||
Rispondi in formato JSON:
|
||
{
|
||
"answer": "Risposta principale dettagliata (3-6 paragrafi)",
|
||
"key_findings": ["Risultato chiave 1", "Risultato chiave 2"],
|
||
"recommendations": ["Raccomandazione pratica 1", "Raccomandazione pratica 2"],
|
||
"risk_areas": ["Area critica 1", "Area critica 2"],
|
||
"benchmark_note": "Nota comparativa settoriale se rilevante, altrimenti null",
|
||
"privacy_note": null
|
||
}
|
||
|
||
Rispondi SOLO con il JSON.
|
||
PROMPT;
|
||
|
||
$response = $this->callAPI($prompt, $system);
|
||
return $this->parseJsonResponse($response);
|
||
}
|
||
|
||
/**
|
||
* Costruisce il contesto aggregato per il prompt cross-org
|
||
*/
|
||
private function buildCrossOrgContext(array $d, int $orgCount): string
|
||
{
|
||
$lines = [];
|
||
|
||
// Distribuzione settoriale
|
||
if (!empty($d['by_sector'])) {
|
||
$lines[] = '### Distribuzione Settoriale';
|
||
foreach ($d['by_sector'] as $sector => $count) {
|
||
$pct = round($count / $orgCount * 100);
|
||
$lines[] = "- {$sector}: {$count} org ({$pct}%)";
|
||
}
|
||
}
|
||
|
||
// Classificazione NIS2
|
||
if (!empty($d['by_entity_type'])) {
|
||
$lines[] = "\n### Classificazione NIS2";
|
||
foreach ($d['by_entity_type'] as $type => $count) {
|
||
$lines[] = "- {$type}: {$count} org";
|
||
}
|
||
}
|
||
|
||
// Compliance score
|
||
if (isset($d['avg_compliance_score'])) {
|
||
$lines[] = "\n### Compliance Score Medio: {$d['avg_compliance_score']}%";
|
||
if (isset($d['score_distribution'])) {
|
||
foreach ($d['score_distribution'] as $range => $count) {
|
||
$lines[] = "- {$range}: {$count} org";
|
||
}
|
||
}
|
||
}
|
||
|
||
// Assessment
|
||
if (isset($d['assessments'])) {
|
||
$a = $d['assessments'];
|
||
$lines[] = "\n### Gap Assessment";
|
||
$lines[] = "- Org con assessment completato: {$a['with_completed']} / {$orgCount}";
|
||
if (!empty($a['avg_by_category'])) {
|
||
$lines[] = "- Score medio per categoria:";
|
||
foreach ($a['avg_by_category'] as $cat => $score) {
|
||
$lines[] = " - {$cat}: {$score}%";
|
||
}
|
||
}
|
||
}
|
||
|
||
// Rischi
|
||
if (isset($d['risks'])) {
|
||
$r = $d['risks'];
|
||
$lines[] = "\n### Rischi (aggregato)";
|
||
$lines[] = "- Totale rischi aperti: {$r['total_open']}";
|
||
$lines[] = "- Media rischi per org: {$r['avg_per_org']}";
|
||
if (!empty($r['by_severity'])) {
|
||
foreach ($r['by_severity'] as $sev => $count) {
|
||
$lines[] = "- Gravità {$sev}: {$count} rischi";
|
||
}
|
||
}
|
||
$lines[] = "- % con piano trattamento: {$r['pct_with_treatment']}%";
|
||
}
|
||
|
||
// Policy
|
||
if (isset($d['policies'])) {
|
||
$p = $d['policies'];
|
||
$lines[] = "\n### Policy";
|
||
$lines[] = "- Media policy approvate per org: {$p['avg_approved']}";
|
||
$lines[] = "- Org senza policy approvate: {$p['without_approved']}";
|
||
}
|
||
|
||
// Formazione
|
||
if (isset($d['training'])) {
|
||
$t = $d['training'];
|
||
$lines[] = "\n### Formazione";
|
||
$lines[] = "- Tasso completamento medio: {$t['avg_completion_rate']}%";
|
||
$lines[] = "- Org con completamento < 50%: {$t['below_50pct']}";
|
||
}
|
||
|
||
// Controlli
|
||
if (isset($d['controls'])) {
|
||
$c = $d['controls'];
|
||
$lines[] = "\n### Controlli ISO 27001 / NIS2";
|
||
$lines[] = "- % media implementazione: {$c['avg_implementation']}%";
|
||
if (!empty($c['weakest_categories'])) {
|
||
$lines[] = "- Categorie più deboli: " . implode(', ', $c['weakest_categories']);
|
||
}
|
||
}
|
||
|
||
// Incidenti
|
||
if (isset($d['incidents'])) {
|
||
$i = $d['incidents'];
|
||
$lines[] = "\n### Incidenti (ultimi 12 mesi)";
|
||
$lines[] = "- Totale incidenti: {$i['total']}";
|
||
$lines[] = "- Org con almeno 1 incidente: {$i['orgs_with_incidents']}";
|
||
if (!empty($i['by_severity'])) {
|
||
foreach ($i['by_severity'] as $sev => $count) {
|
||
$lines[] = "- Gravità {$sev}: {$count}";
|
||
}
|
||
}
|
||
}
|
||
|
||
return implode("\n", $lines);
|
||
}
|
||
|
||
/**
|
||
* Registra interazione AI nel database
|
||
*/
|
||
public function logInteraction(int $orgId, int $userId, string $type, string $promptSummary, string $responseSummary, int $tokensUsed = 0): void
|
||
{
|
||
Database::insert('ai_interactions', [
|
||
'organization_id' => $orgId,
|
||
'user_id' => $userId,
|
||
'interaction_type' => $type,
|
||
'prompt_summary' => substr($promptSummary, 0, 500),
|
||
'response_summary' => $responseSummary,
|
||
'tokens_used' => $tokensUsed,
|
||
'model_used' => $this->model,
|
||
]);
|
||
}
|
||
}
|