281 lines
12 KiB
PHP
281 lines
12 KiB
PHP
<?php
|
|
/**
|
|
* Fociz AI Co-Pilot Engine (Release v1.5)
|
|
* Beheert intelligente assistentie voor Brainstorming en Dagplanning.
|
|
*/
|
|
|
|
require_once __DIR__ . '/http.php';
|
|
require_once __DIR__ . '/settings.php';
|
|
|
|
/**
|
|
* Voert een gestructureerde chat completion uit met de geconfigureerde AI provider
|
|
*
|
|
* @param array $messages [['role' => 'system'|'user'|'assistant', 'content' => string]]
|
|
* @param array $options ['temperature' => float, 'json_mode' => bool, 'max_tokens' => int]
|
|
* @return array ['success' => bool, 'content' => ?string, 'json' => ?array, 'error' => ?string]
|
|
*/
|
|
function fociz_ai_chat(array $messages, array $options = []): array {
|
|
$aiSettings = get_ai_settings();
|
|
if (empty($aiSettings['ai_api_key'])) {
|
|
return [
|
|
'success' => false,
|
|
'error' => 'Geen AI API-sleutel geconfigureerd. Stel deze in via Systeembeheer (admin.php).',
|
|
];
|
|
}
|
|
|
|
$apiKey = $aiSettings['ai_api_key'];
|
|
$model = !empty($aiSettings['ai_model']) ? $aiSettings['ai_model'] : 'gpt-4o-mini';
|
|
$temperature = $options['temperature'] ?? 0.3;
|
|
$maxTokens = $options['max_tokens'] ?? 1500;
|
|
$jsonMode = !empty($options['json_mode']);
|
|
|
|
$payload = [
|
|
'model' => $model,
|
|
'messages' => $messages,
|
|
'temperature' => $temperature,
|
|
'max_tokens' => $maxTokens,
|
|
];
|
|
|
|
if ($jsonMode) {
|
|
$payload['response_format'] = ['type' => 'json_object'];
|
|
}
|
|
|
|
$response = fociz_http_request('https://api.openai.com/v1/chat/completions', [
|
|
'method' => 'POST',
|
|
'headers' => [
|
|
'Authorization' => "Bearer {$apiKey}",
|
|
'Content-Type' => 'application/json',
|
|
],
|
|
'body' => $payload,
|
|
'timeout' => 35,
|
|
]);
|
|
|
|
if ($response['status'] !== 200 || empty($response['json']['choices'][0]['message']['content'])) {
|
|
$errMsg = $response['json']['error']['message'] ?? ($response['error'] ?? "AI aanroep mislukt (HTTP {$response['status']})");
|
|
error_log("fociz_ai_chat fout: {$errMsg}");
|
|
return [
|
|
'success' => false,
|
|
'error' => $errMsg,
|
|
];
|
|
}
|
|
|
|
$rawContent = trim($response['json']['choices'][0]['message']['content']);
|
|
$parsedJson = null;
|
|
|
|
if ($jsonMode) {
|
|
$parsedJson = json_decode($rawContent, true);
|
|
}
|
|
|
|
return [
|
|
'success' => true,
|
|
'content' => $rawContent,
|
|
'json' => $parsedJson,
|
|
'model' => $response['json']['model'] ?? $model,
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Categoriseert sticky notes automatisch in:
|
|
* - 'Ideeën'
|
|
* - 'Knelpunten'
|
|
* - 'Oplossingen'
|
|
* - 'Quick Wins'
|
|
*/
|
|
function fociz_ai_categorize_cards(string $sessionTitle, string $theme, array $cards): array {
|
|
if (empty($cards)) {
|
|
return ['success' => true, 'categorized' => []];
|
|
}
|
|
|
|
$cardItems = [];
|
|
foreach ($cards as $c) {
|
|
$cardItems[] = [
|
|
'id' => $c['id'],
|
|
'content' => $c['content'],
|
|
'category' => $c['category'] ?? 'Ideeën',
|
|
];
|
|
}
|
|
|
|
$systemPrompt = "Je bent een deskundige agile facilitator en brainstorm co-pilot voor software- en productontwikkeling.\n"
|
|
. "Je ontvangt een lijst met brainstorm kaartjes voor de sessie: '{$sessionTitle}' (Thema: '{$theme}').\n"
|
|
. "Je taak is om elk kaartje in te delen in exact één van deze 4 categorieën:\n"
|
|
. "1. 'Ideeën' (nieuwe concepten, functies, wensen)\n"
|
|
. "2. 'Knelpunten' (problemen, blokkades, risico's, frustraties)\n"
|
|
. "3. 'Oplossingen' (concrete antwoorden op knelpunten, architectuurkeuzes)\n"
|
|
. "4. 'Quick Wins' (kleine, makkelijk uitvoerbare verbeteringen met hoge impact)\n\n"
|
|
. "Retourneer uitsluitend een JSON-object met het veld 'categorized' dat een array is van objecten met 'id' en 'category'.";
|
|
|
|
$userPrompt = "Hier zijn de kaartjes:\n" . json_encode($cardItems, JSON_UNESCAPED_UNICODE);
|
|
|
|
$aiRes = fociz_ai_chat([
|
|
['role' => 'system', 'content' => $systemPrompt],
|
|
['role' => 'user', 'content' => $userPrompt],
|
|
], ['json_mode' => true]);
|
|
|
|
if (!$aiRes['success'] || empty($aiRes['json']['categorized'])) {
|
|
return ['success' => false, 'error' => $aiRes['error'] ?? 'Kon kaartjes niet categoriseren'];
|
|
}
|
|
|
|
return ['success' => true, 'categorized' => $aiRes['json']['categorized']];
|
|
}
|
|
|
|
/**
|
|
* Genereert nieuwe relevante brainstorm ideeën voor een sessie
|
|
*/
|
|
function fociz_ai_generate_ideas(string $sessionTitle, string $theme, int $count = 4): array {
|
|
$systemPrompt = "Je bent een innovatieve productstrateeg en brainstorm co-pilot.\n"
|
|
. "Genereer {$count} creatieve, scherpe en direct toepasbare ideeën voor een brainstormsessie getiteld: '{$sessionTitle}' (Thema: '{$theme}').\n"
|
|
. "Houd de teksten beknopt en krachtig (1-2 zinnen per idee, alsof ze op een gele post-it staan).\n"
|
|
. "Retourneer uitsluitend een JSON-object met het veld 'ideas' dat een array van strings is.";
|
|
|
|
$aiRes = fociz_ai_chat([
|
|
['role' => 'system', 'content' => $systemPrompt],
|
|
['role' => 'user', 'content' => "Genereer {$count} vernieuwende ideeën."],
|
|
], ['json_mode' => true]);
|
|
|
|
if (!$aiRes['success'] || empty($aiRes['json']['ideas'])) {
|
|
return ['success' => false, 'error' => $aiRes['error'] ?? 'Kon geen ideeën genereren'];
|
|
}
|
|
|
|
return ['success' => true, 'ideas' => $aiRes['json']['ideas']];
|
|
}
|
|
|
|
/**
|
|
* Zet brainstorm kaartjes om in een scherp Roadmap Item
|
|
*/
|
|
function fociz_ai_summarize_to_roadmap(string $content, string $sessionTitle = ''): array {
|
|
$systemPrompt = "Je bent een senior product manager. Zet onderstaand brainstorm idee om in een helder, professioneel Roadmap Item.\n"
|
|
. "Retourneer een JSON-object met:\n"
|
|
. "- 'title': Een beknopte, actiegerichte titel (max 8 woorden)\n"
|
|
. "- 'description': Een duidelijke beschrijving van wat er gebouwd moet worden inclusief gebruikerswaarde en 2-3 acceptatiecriteria\n"
|
|
. "- 'priority': 'HIGH', 'MEDIUM' of 'LOW'\n"
|
|
. "- 'type': 'FEATURE', 'IMPROVEMENT' of 'TECH_DEBT'\n"
|
|
. "- 'quarter': 'Q3' of 'Q4'";
|
|
|
|
$aiRes = fociz_ai_chat([
|
|
['role' => 'system', 'content' => $systemPrompt],
|
|
['role' => 'user', 'content' => "Sessie: {$sessionTitle}\nIdee:\n{$content}"],
|
|
], ['json_mode' => true]);
|
|
|
|
if (!$aiRes['success'] || empty($aiRes['json']['title'])) {
|
|
return ['success' => false, 'error' => $aiRes['error'] ?? 'Kon roadmap item niet formuleren'];
|
|
}
|
|
|
|
return ['success' => true, 'item' => $aiRes['json']];
|
|
}
|
|
|
|
/**
|
|
* Berekent een slimme dagindeling voor open taken rondom bestaande afspraken
|
|
*/
|
|
function fociz_ai_smart_schedule(array $tasks, array $events, array $workdaySettings): array {
|
|
$startH = (int)explode(':', $workdaySettings['workday_start'] ?? '08:30')[0];
|
|
$endH = (int)explode(':', $workdaySettings['workday_end'] ?? '17:00')[0];
|
|
$lunchStart = $workdaySettings['lunch_start'] ?? '12:30';
|
|
$lunchEnd = $workdaySettings['lunch_end'] ?? '13:00';
|
|
|
|
$systemPrompt = "Je bent een productiviteits- en time-blocking assistent.\n"
|
|
. "Maak een optimale dagplanning door openstaande taken in te plannen tussen de werkdaguren ({$workdaySettings['workday_start']} tot {$workdaySettings['workday_end']}).\n"
|
|
. "Regels:\n"
|
|
. "1. Plan GEEN taken tijdens bestaande afspraken of tijdens de lunchpauze ({$lunchStart} - {$lunchEnd}).\n"
|
|
. "2. Geef prioriteit aan P1 (brandend/focus) vroeg op de dag, daarna P2 en P3.\n"
|
|
. "3. Zorg voor realistische tijdsblokken (15, 30, 45 of 60 minuten) met format 'HH:mm'.\n"
|
|
. "Retourneer een JSON-object met het veld 'scheduled' als array van objecten: {'id': int, 'time_block_start': 'HH:mm', 'duration_minutes': int}.";
|
|
|
|
$userPrompt = "Bestaande afspraken:\n" . json_encode($events, JSON_UNESCAPED_UNICODE) . "\n\n"
|
|
. "In te plannen taken:\n" . json_encode($tasks, JSON_UNESCAPED_UNICODE);
|
|
|
|
$aiRes = fociz_ai_chat([
|
|
['role' => 'system', 'content' => $systemPrompt],
|
|
['role' => 'user', 'content' => $userPrompt],
|
|
], ['json_mode' => true]);
|
|
|
|
if (!$aiRes['success'] || empty($aiRes['json']['scheduled'])) {
|
|
return ['success' => false, 'error' => $aiRes['error'] ?? 'Kon geen slimme indeling berekenen'];
|
|
}
|
|
|
|
return ['success' => true, 'scheduled' => $aiRes['json']['scheduled']];
|
|
}
|
|
|
|
/**
|
|
* Genereert een persoonlijke AI dagbriefing op basis van taken, afspraken en Jira tickets
|
|
*
|
|
* @param string $userName Naam van de gebruiker
|
|
* @param string $date Datum (Y-m-d)
|
|
* @param array $tasks Openstaande taken van vandaag
|
|
* @param array $meetings Outlook meetings van vandaag
|
|
* @param array $jiraIssues Jira tickets (P1/blokkades)
|
|
* @param array $unfinishedYest Gisteren niet voltooide taken
|
|
* @return array ['success' => bool, 'briefing' => string, 'focus_tip' => string, 'warnings' => array]
|
|
*/
|
|
function fociz_ai_daily_briefing(
|
|
string $userName,
|
|
string $date,
|
|
array $tasks,
|
|
array $meetings,
|
|
array $jiraIssues = [],
|
|
array $unfinishedYest = []
|
|
): array {
|
|
$dutchDays = ['', 'maandag', 'dinsdag', 'woensdag', 'donderdag', 'vrijdag', 'zaterdag', 'zondag'];
|
|
$dayName = $dutchDays[(int)date('N', strtotime($date))] ?? 'vandaag';
|
|
$dateLabel = $dayName . ' ' . date('j', strtotime($date)) . ' ' . date('F', strtotime($date));
|
|
|
|
$taskSummary = array_map(fn($t) => [
|
|
'title' => $t['title'],
|
|
'priority' => $t['priority'] ?? 'P2',
|
|
'time' => $t['time_block_start'] ?? null,
|
|
'duration' => (int)($t['duration_minutes'] ?? 30),
|
|
], array_slice($tasks, 0, 10));
|
|
|
|
$meetingSummary = array_map(fn($e) => [
|
|
'subject' => $e['subject'] ?? 'Afspraak',
|
|
'start' => $e['start'] ?? null,
|
|
'end' => $e['end'] ?? null,
|
|
], array_slice($meetings, 0, 8));
|
|
|
|
$jiraSummary = array_map(fn($j) => [
|
|
'key' => $j['key'] ?? '',
|
|
'title' => $j['summary'] ?? '',
|
|
'priority' => $j['priority'] ?? 'Medium',
|
|
'status' => $j['status'] ?? 'Open',
|
|
], array_slice(array_filter($jiraIssues, fn($j) => in_array($j['priority'] ?? '', ['Highest', 'High'])), 0, 5));
|
|
|
|
$yesterdaySummary = array_map(fn($t) => $t['title'], array_slice($unfinishedYest, 0, 5));
|
|
|
|
$systemPrompt = "Je bent de persoonlijke AI Co-Pilot van {$userName} in Fociz. "
|
|
. "Schrijf een korte, energieke en persoonlijke dagbriefing in het Nederlands voor {$dateLabel}. "
|
|
. "Wees direct, concreet en vriendelijk. Gebruik geen markdown headers, geen bullet symbolen zoals *, -, •. "
|
|
. "Geef 1-2 zinnen dagopening + 1 zin over de grootste prioriteit + 1 praktisch focustip. "
|
|
. "Maximaal 60 woorden totaal. "
|
|
. "Retourneer een JSON-object met:\n"
|
|
. "- 'greeting': openingszin (max 20 woorden)\n"
|
|
. "- 'focus_task': de meest urgente taak of meeting vandaag (max 15 woorden)\n"
|
|
. "- 'tip': één concrete focustip voor vandaag (max 20 woorden)\n"
|
|
. "- 'warnings': array van strings, max 2 items — alleen als er iets opvalt (gisteren niet afgerond, urgent Jira, overvolle dag)";
|
|
|
|
$userPrompt = json_encode([
|
|
'date' => $dateLabel,
|
|
'tasks_today' => $taskSummary,
|
|
'meetings_today' => $meetingSummary,
|
|
'urgent_jira' => $jiraSummary,
|
|
'unfinished_yesterday' => $yesterdaySummary,
|
|
'total_tasks' => count($tasks),
|
|
'total_meetings' => count($meetings),
|
|
], JSON_UNESCAPED_UNICODE);
|
|
|
|
$aiRes = fociz_ai_chat([
|
|
['role' => 'system', 'content' => $systemPrompt],
|
|
['role' => 'user', 'content' => $userPrompt],
|
|
], ['json_mode' => true, 'temperature' => 0.6, 'max_tokens' => 300]);
|
|
|
|
if (!$aiRes['success'] || empty($aiRes['json'])) {
|
|
return ['success' => false, 'error' => $aiRes['error'] ?? 'Kon geen dagbriefing genereren'];
|
|
}
|
|
|
|
return [
|
|
'success' => true,
|
|
'greeting' => $aiRes['json']['greeting'] ?? "Goedemorgen {$userName}, een nieuwe dag!",
|
|
'focus_task'=> $aiRes['json']['focus_task'] ?? '',
|
|
'tip' => $aiRes['json']['tip'] ?? '',
|
|
'warnings' => $aiRes['json']['warnings'] ?? [],
|
|
];
|
|
}
|