Добавлен сервис AI
Добавлен ai агент для формирования и проверки статистики Добавлен ai агент для поддержки
This commit is contained in:
55
app/Ai/Agents/RouterAgent.php
Normal file
55
app/Ai/Agents/RouterAgent.php
Normal file
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
namespace App\Ai\Agents;
|
||||
|
||||
use Laravel\Ai\Contracts\Agent;
|
||||
use Laravel\Ai\Contracts\Conversational;
|
||||
use Laravel\Ai\Contracts\HasTools;
|
||||
use Laravel\Ai\Contracts\Tool;
|
||||
use Laravel\Ai\Messages\Message;
|
||||
use Laravel\Ai\Promptable;
|
||||
use Stringable;
|
||||
|
||||
class RouterAgent implements Agent, Conversational, HasTools
|
||||
{
|
||||
use Promptable;
|
||||
|
||||
/**
|
||||
* Get the instructions that the agent should follow.
|
||||
*/
|
||||
public function instructions(): Stringable|string
|
||||
{
|
||||
return 'Ты — маршрутизатор запросов в системе медицинской статистики.
|
||||
Твоя задача — определить, к какой категории относится вопрос пользователя:
|
||||
|
||||
1. "stats" – если вопрос касается показателей работы отделений: загрузка, койко-дни, операции, поступления, выбытия, предоперационные дни, сравнения, тренды.
|
||||
2. "support" – если вопрос касается работы с системой, инструкций, регламентов, ошибок, как что-то сделать.
|
||||
3. "other" – если вопрос не относится ни к статистике, ни к поддержке (например, общие вопросы, приветствия).
|
||||
|
||||
Ответ должен быть строго в формате JSON:
|
||||
{"route": "stats", "confidence": 0.95, "reason": "вопрос про загрузку отделения"}
|
||||
|
||||
Оценивай уверенность (confidence) от 0 до 1. Если сомневаешься, выбери наиболее вероятный маршрут.
|
||||
Отвечай только JSON.';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the list of messages comprising the conversation so far.
|
||||
*
|
||||
* @return Message[]
|
||||
*/
|
||||
public function messages(): iterable
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the tools available to the agent.
|
||||
*
|
||||
* @return Tool[]
|
||||
*/
|
||||
public function tools(): iterable
|
||||
{
|
||||
return [];
|
||||
}
|
||||
}
|
||||
90
app/Ai/Agents/StatisticsAnalyst.php
Normal file
90
app/Ai/Agents/StatisticsAnalyst.php
Normal file
@@ -0,0 +1,90 @@
|
||||
<?php
|
||||
|
||||
namespace App\Ai\Agents;
|
||||
|
||||
use App\Services\DutyReportService;
|
||||
use Laravel\Ai\Contracts\Agent;
|
||||
use Laravel\Ai\Contracts\Conversational;
|
||||
use Laravel\Ai\Contracts\HasTools;
|
||||
use Laravel\Ai\Contracts\Tool;
|
||||
use Laravel\Ai\Messages\Message;
|
||||
use Laravel\Ai\Promptable;
|
||||
use App\Ai\Tools\GetDepartmentStatsTool;
|
||||
use Stringable;
|
||||
|
||||
class StatisticsAnalyst implements Agent, Conversational, HasTools
|
||||
{
|
||||
use Promptable;
|
||||
|
||||
public function __construct(
|
||||
protected DutyReportService $reportService,
|
||||
protected string $role = 'guest'
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Get the instructions that the agent should follow.
|
||||
*/
|
||||
public function instructions(): Stringable|string
|
||||
{
|
||||
return <<<'PROMPT'
|
||||
Ты — эксперт по медицинской статистике стационара. Отвечаешь на вопросы пользователей о показателях работы отделений.
|
||||
|
||||
**Правила работы:**
|
||||
1. Период отчёта — это смена: с 09:00 текущего дня по 09:00 следующего дня (сутки). Если пользователь не указал период, используй переданный в вопросе диапазон.
|
||||
2. Для получения данных используй инструмент GetDepartmentStatsTool. Он возвращает статистику за указанный период.
|
||||
3. Анализируй данные и давай развёрнутые, понятные ответы на русском языке.
|
||||
4. Если данных недостаточно, предложи уточнить вопрос или указать отделение/период.
|
||||
5. Если роль пользователя ({$this->role}) не позволяет видеть некоторые данные, сообщи об этом вежливо.
|
||||
|
||||
**Формат ответа (строго JSON, без Markdown, без пояснений вне JSON):**
|
||||
{
|
||||
"text": "Твой развернутый ответ на русском языке. Используй простые предложения, без маркдауна.",
|
||||
"data": {
|
||||
// объект с числовыми показателями, которые ты упомянул в ответе,
|
||||
// или null, если их нет.
|
||||
// Ключи — короткие названия на английском (например, occupancy_percent, avg_bed_day, beds, patients).
|
||||
}
|
||||
}
|
||||
|
||||
**Важно:**
|
||||
- Никогда не используй Markdown (**, *, -, # и т.д.) внутри поля "text".
|
||||
- Все числовые значения, которые ты приводишь в ответе, дублируй в поле "data" (если они относятся к показателям).
|
||||
- Если пользователь спрашивает про средний койко-день — уточни, за какой период он рассчитан (за смену или за другой период), и приведи соответствующее значение.
|
||||
- Не добавляй никаких пояснений вне JSON.
|
||||
|
||||
Пример ответа:
|
||||
{
|
||||
"text": "В урологическом отделении за смену 19 июля загрузка составляет 89.58% при 48 койках. Средняя длительность пребывания пациентов, находящихся в отделении в эту смену, равна 0.96 дня.",
|
||||
"data": {
|
||||
"department": "Урологическое",
|
||||
"beds": 48,
|
||||
"patients_in_department": 43,
|
||||
"avg_bed_day": 0.96,
|
||||
"occupancy_percent": 89.58
|
||||
}
|
||||
}
|
||||
PROMPT;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the list of messages comprising the conversation so far.
|
||||
*
|
||||
* @return Message[]
|
||||
*/
|
||||
public function messages(): iterable
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the tools available to the agent.
|
||||
*
|
||||
* @return Tool[]
|
||||
*/
|
||||
public function tools(): iterable
|
||||
{
|
||||
return [
|
||||
new GetDepartmentStatsTool($this->reportService),
|
||||
];
|
||||
}
|
||||
}
|
||||
49
app/Ai/Agents/SupportAgent.php
Normal file
49
app/Ai/Agents/SupportAgent.php
Normal file
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
namespace App\Ai\Agents;
|
||||
|
||||
use Laravel\Ai\Contracts\Agent;
|
||||
use Laravel\Ai\Contracts\Conversational;
|
||||
use Laravel\Ai\Contracts\HasTools;
|
||||
use Laravel\Ai\Contracts\Tool;
|
||||
use Laravel\Ai\Messages\Message;
|
||||
use Laravel\Ai\Promptable;
|
||||
use Stringable;
|
||||
|
||||
class SupportAgent implements Agent, Conversational, HasTools
|
||||
{
|
||||
use Promptable;
|
||||
|
||||
/**
|
||||
* Get the instructions that the agent should follow.
|
||||
*/
|
||||
public function instructions(): Stringable|string
|
||||
{
|
||||
return 'Ты — ассистент поддержки пользователей медицинской информационной системы.
|
||||
Отвечаешь на вопросы о работе с системой: как создать отчёт, как посмотреть статистику, как исправить ошибку, какие регламенты действуют.
|
||||
|
||||
Используй информацию из внутренней базы знаний (если она есть). Если не знаешь — предложи обратиться к администратору.
|
||||
|
||||
Отвечай на русском языке, чётко и по делу.';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the list of messages comprising the conversation so far.
|
||||
*
|
||||
* @return Message[]
|
||||
*/
|
||||
public function messages(): iterable
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the tools available to the agent.
|
||||
*
|
||||
* @return Tool[]
|
||||
*/
|
||||
public function tools(): iterable
|
||||
{
|
||||
return [];
|
||||
}
|
||||
}
|
||||
98
app/Ai/Tools/GetDepartmentStatsTool.php
Normal file
98
app/Ai/Tools/GetDepartmentStatsTool.php
Normal file
@@ -0,0 +1,98 @@
|
||||
<?php
|
||||
|
||||
namespace App\Ai\Tools;
|
||||
|
||||
use App\Models\ReportDuty;
|
||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
use App\Services\DutyReportService;
|
||||
use Laravel\Ai\Contracts\Tool;
|
||||
use Laravel\Ai\Tools\Request;
|
||||
use Stringable;
|
||||
|
||||
class GetDepartmentStatsTool implements Tool
|
||||
{
|
||||
public function __construct(
|
||||
private readonly DutyReportService $reportService
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Get the description of the tool's purpose.
|
||||
*/
|
||||
public function description(): Stringable|string
|
||||
{
|
||||
return 'Получить статистику по конкретному отделению за указанный период. ' .
|
||||
'Возвращает: название отделения, количество коек, поступивших, выбывших, ' .
|
||||
'средний койко-день, процент загруженности и т.д.' .
|
||||
'Средний койко-день считается по формуле: total_bed_days / (patient_outcome + patients_in_department)';
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the tool.
|
||||
*/
|
||||
public function handle(Request $request): Stringable|string
|
||||
{
|
||||
// 1. Получаем параметры из запроса агента
|
||||
$departmentName = $request['department_name'];
|
||||
$startDate = $request['start_date'];
|
||||
$endDate = $request['end_date'];
|
||||
|
||||
// 2. Здесь вы используете вашу существующую логику для получения отчета.
|
||||
// Например, вы можете найти ID отделения по имени и вызвать ваш DutyReportService.
|
||||
// Это упрощенный пример, вам нужно адаптировать его под вашу архитектуру.
|
||||
|
||||
// Пример: получаем последний отчет для этого отделения за период.
|
||||
$report = ReportDuty::query()
|
||||
->whereHas('department', function ($query) use ($departmentName) {
|
||||
$query->where('name_full', 'ilike', "$departmentName%");
|
||||
})
|
||||
->where('period_start', '>=', $startDate)
|
||||
->where('period_end', '<=', $endDate)
|
||||
->latest()
|
||||
->first();
|
||||
|
||||
if (!$report) {
|
||||
return "Данные по отделению '{$departmentName}' за указанный период не найдены.";
|
||||
}
|
||||
|
||||
// dd($report, $report->metrikaResult);
|
||||
|
||||
// 3. Возвращаем агенту структурированные данные (например, в JSON).
|
||||
// Агент проанализирует эти цифры и даст ответ.
|
||||
return json_encode([
|
||||
'department' => $departmentName,
|
||||
'beds' => $report->metrikaResult->where('rf_metrika_item_id', 1)->first()?->value ?? 0,
|
||||
'patients_in_department' => $report->metrikaResult->where('rf_metrika_item_id', 8)->first()?->value ?? 0,
|
||||
'patient_outcome' => $report->metrikaResult->where('rf_metrika_item_id', 7)->first()?->value ?? 0,
|
||||
'avg_bed_day' => $report->metrikaResult->where('rf_metrika_item_id', 18)->first()?->value ?? 0,
|
||||
'occupancy_percent' => $report->metrikaResult->where('rf_metrika_item_id', 22)->first()?->value ?? 0,
|
||||
'patients_is_recipient' => $report->metrikaResult->where('rf_metrika_item_id', 3)->first()?->value ?? 0,
|
||||
'patients_is_discharged' => $report->metrikaResult->where('rf_metrika_item_id', 15)->first()?->value ?? 0,
|
||||
'patients_is_transferred' => $report->metrikaResult->where('rf_metrika_item_id', 13)->first()?->value ?? 0,
|
||||
'patients_is_deceased' => $report->metrikaResult->where('rf_metrika_item_id', 9)->first()?->value ?? 0,
|
||||
'planned_patients' => $report->metrikaResult->where('rf_metrika_item_id', 4)->first()?->value ?? 0,
|
||||
'urgent_patients' => $report->metrikaResult->where('rf_metrika_item_id', 12)->first()?->value ?? 0,
|
||||
'total_bed_days' => $report->metrikaResult->where('rf_metrika_item_id', 25)->first()?->value ?? 0,
|
||||
'total_pre_op_days' => $report->metrikaResult->where('rf_metrika_item_id', 26)->first()?->value ?? 0,
|
||||
'patients_with_ops' => $report->metrikaResult->where('rf_metrika_item_id', 27)->first()?->value ?? 0,
|
||||
'avg_pre_op_bed_day' => $report->metrikaResult->where('rf_metrika_item_id', 21)->first()?->value ?? 0,
|
||||
'mortality_percent' => $report->metrikaResult->where('rf_metrika_item_id', 19)->first()?->value ?? 0,
|
||||
'planned_operations' => $report->metrikaResult->where('rf_metrika_item_id', 11)->first()?->value ?? 0,
|
||||
'urgent_operations' => $report->metrikaResult->where('rf_metrika_item_id', 10)->first()?->value ?? 0,
|
||||
'staff' => $report->metrikaResult->where('rf_metrika_item_id', 17)->first()?->value ?? 0,
|
||||
|
||||
// $this->saveMetric($reportDuty->id, 18, $avgBedDay); // Ср. койко-день
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the tool's schema definition.
|
||||
*/
|
||||
public function schema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
'department_name' => $schema->string()->required()->description('Название отделения, например: "Отоларингологическое"'),
|
||||
'start_date' => $schema->string()->required()->description('Дата начала периода в формате YYYY-MM-DD HH:mm'),
|
||||
'end_date' => $schema->string()->required()->description('Дата окончания периода в формате YYYY-MM-DD HH:mm'),
|
||||
];
|
||||
}
|
||||
}
|
||||
79
app/Http/Controllers/Api/Ai/AiController.php
Normal file
79
app/Http/Controllers/Api/Ai/AiController.php
Normal file
@@ -0,0 +1,79 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api\Ai;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use App\Services\Ai\AiDispatcherService;
|
||||
use App\Services\DateRangeService;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class AiController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
protected AiDispatcherService $dispatcher,
|
||||
protected DateRangeService $dateRangeService
|
||||
) {}
|
||||
|
||||
public function ask(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'question' => 'required|string|max:500',
|
||||
'period_start' => 'nullable',
|
||||
'period_end' => 'nullable',
|
||||
]);
|
||||
|
||||
$user = $request->user();
|
||||
$role = $user?->role ?? 'guest';
|
||||
$question = $request->question;
|
||||
|
||||
$periodStart = $request->period_start;
|
||||
$periodEnd = $request->period_end;
|
||||
$dates = $this->dateRangeService->getDateRangeForUser($user, $periodStart, $periodEnd, false);
|
||||
|
||||
$cacheKey = 'ai_stats_' . md5("{$role}|{$periodStart}|{$periodEnd}|{$question}");
|
||||
|
||||
$cached = Cache::get($cacheKey);
|
||||
if ($cached) {
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'from_cache' => true,
|
||||
'answer' => $cached['answer'],
|
||||
'structured' => $cached['structured'] ?? null,
|
||||
]);
|
||||
}
|
||||
|
||||
try {
|
||||
$result = $this->dispatcher->dispatch(
|
||||
question: "Период с {$dates[0]} по {$dates[1]}. Вопрос: {$question}",
|
||||
role: $role
|
||||
);
|
||||
|
||||
// Cache::put($cacheKey, [
|
||||
// 'answer' => $result['answer'],
|
||||
// 'structured' => $result['structured'] ?? null,
|
||||
// ], now()->addMinutes(10));
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'from_cache' => false,
|
||||
'answer' => $result['answer'],
|
||||
'structured' => $result['structured'] ?? null,
|
||||
]);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
Log::error('AI Dispatch Error: ' . $e->getMessage(), [
|
||||
'user' => $user?->id,
|
||||
'question' => $question,
|
||||
'period' => "$periodStart - $periodEnd",
|
||||
]);
|
||||
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Не удалось обработать запрос. Попробуйте позже.',
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
}
|
||||
58
app/Services/Ai/AiDispatcherService.php
Normal file
58
app/Services/Ai/AiDispatcherService.php
Normal file
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Ai;
|
||||
|
||||
use App\Ai\Agents\RouterAgent;
|
||||
use App\Ai\Agents\StatisticsAnalyst;
|
||||
use App\Ai\Agents\SupportAgent;
|
||||
use App\Services\DutyReportService;
|
||||
|
||||
class AiDispatcherService
|
||||
{
|
||||
public function __construct(
|
||||
private readonly DutyReportService $reportService
|
||||
) {}
|
||||
|
||||
public function dispatch(string $question, string $role = 'guest'): array
|
||||
{
|
||||
// 1. Маршрутизация
|
||||
$router = new RouterAgent();
|
||||
$routerResponse = $router->prompt($question)->text; // получаем текстовый ответ
|
||||
|
||||
// Парсим JSON из ответа роутера
|
||||
$routeData = json_decode($routerResponse, true);
|
||||
$route = $routeData['route'] ?? 'other';
|
||||
|
||||
// 2. Выбор агента
|
||||
$agent = match ($route) {
|
||||
'stats' => new StatisticsAnalyst($this->reportService, $role),
|
||||
'support' => new SupportAgent(),
|
||||
default => null,
|
||||
};
|
||||
|
||||
if (!$agent) {
|
||||
return [
|
||||
'answer' => 'Извините, я не могу ответить на этот вопрос. Пожалуйста, уточните, что вы имеете в виду.',
|
||||
'structured' => null,
|
||||
];
|
||||
}
|
||||
|
||||
// 3. Запрос к специалисту
|
||||
$responseText = $agent->prompt($question)->text;
|
||||
|
||||
// Пытаемся распарсить JSON (если агент вернул структурированный ответ)
|
||||
$parsed = json_decode($responseText, true);
|
||||
if (json_last_error() === JSON_ERROR_NONE && isset($parsed['text'])) {
|
||||
return [
|
||||
'answer' => $parsed['text'],
|
||||
'structured' => $parsed['data'] ?? null,
|
||||
];
|
||||
}
|
||||
|
||||
// Если не JSON – возвращаем как есть
|
||||
return [
|
||||
'answer' => $responseText,
|
||||
'structured' => null,
|
||||
];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user