Files
crm-ms1/v4_ci4/app/Libraries/Radar/RadarAnalyzeService.php

107 lines
3.4 KiB
PHP

<?php
namespace App\Libraries\Radar;
use App\Libraries\Ai\AiClient;
use App\Libraries\Ai\AiException;
use App\Libraries\Ai\AiFactory;
/**
* MSOP-3 — analyse métier dossards + delta.
* Utilise la couche IA plateforme (AiClient), pas un vendor direct.
*/
class RadarAnalyzeService
{
public function __construct(private ?AiClient $ai = null)
{
$this->ai ??= AiFactory::client();
}
public function isEnabled(): bool
{
return $this->ai->isConfigured();
}
/**
* @param list<array{subject:string,summary:string,analysis:?string,received_at:?string}> $priors
* @return array{analysis:string, delta:string, confidence:int}
*/
public function analyzeDossards(
string $subject,
string $fromEmail,
string $body,
array $priors,
): array {
$hist = '';
foreach ($priors as $i => $p) {
$n = $i + 1;
$hist .= "--- Mail dossards #{$n} ({$p['received_at']}) ---\n"
. 'Sujet: ' . ($p['subject'] ?? '') . "\n"
. 'Analyse/snippet: ' . (($p['analysis'] ?: $p['summary']) ?? '') . "\n\n";
}
$isFirst = $priors === [];
$system = <<<'SYS'
Tu es l'assistant Radar du CRM MS1 Timing (chronométrage / dossards).
Tu SIGNALES seulement. Tu n'APPROUVES JAMAIS une impression ni un Go.
Réponds UNIQUEMENT en JSON valide UTF-8, sans markdown autour, schéma :
{"analysis":"string","delta":"string","confidence":0}
SYS;
$prompt = <<<PROMPT
- analysis : analyse métier dossards courte (FR), points utiles : logos, séquences, imprimeur, délais, fichiers, questions ouvertes, signaux faibles vs forts. Pas de copier-coller du mail. Max ~1200 caractères.
- delta : si 1er mail → chaîne vide "". Sinon : ce qui change / stable / nouveau / contradictoire vs l'historique. Max ~800 caractères.
- confidence : entier 0-100 (fiabilité de ton analyse, pas un feu vert).
Contexte :
Expéditeur : {$fromEmail}
Sujet : {$subject}
Premier mail dossards pour cet expéditeur : {$this->boolFr($isFirst)}
Historique dossards déjà en Radar (du plus ancien au plus récent) :
{$hist}
Mail courant (corps) :
{$body}
PROMPT;
try {
$out = $this->ai->complete($prompt, ['system' => $system]);
} catch (AiException $e) {
throw new \RuntimeException($e->getMessage(), (int) $e->getCode(), $e);
}
$text = $out->text;
if (\preg_match('/\{.*\}/s', $text, $m)) {
$text = $m[0];
}
$json = \json_decode($text, true);
if (! \is_array($json)) {
return [
'analysis' => \mb_substr($out->text, 0, 2000),
'delta' => $isFirst ? '' : 'Delta non structuré — voir analyse.',
'confidence' => 40,
];
}
$analysis = \trim((string) ($json['analysis'] ?? ''));
$delta = \trim((string) ($json['delta'] ?? ''));
$conf = (int) ($json['confidence'] ?? 50);
$conf = \max(0, \min(100, $conf));
if ($isFirst) {
$delta = '';
}
return [
'analysis' => \mb_substr($analysis !== '' ? $analysis : $out->text, 0, 4000),
'delta' => \mb_substr($delta, 0, 2000),
'confidence' => $conf,
];
}
private function boolFr(bool $v): string
{
return $v ? 'oui' : 'non';
}
}