#Если Клиент Тогда 
//ирПортативный Перем ирПортативный Экспорт;
//ирПортативный Перем ирОбщий Экспорт;
//ирПортативный Перем ирСервер Экспорт;
//ирПортативный Перем ирКэш Экспорт;
//ирПортативный Перем ирКлиент Экспорт;
	
Перем мБраузер Экспорт; /// Кэш 
Перем мЛиПроверочный; // Булево
Перем мОписаниеЗадания;
Перем мЛиПотоковый;
Перем мНакопленныйОтвет;

Функция Браузер() Экспорт
	Возврат мБраузер._1CAPI; /// Добавка нужна для поддержки 8.3.25-
КонецФункции

// Ответ нужно ловить в обработчике onclick браузера.
// Отменяет текущий активный запрос.
// Параметры:
//   Адрес - Строка - 
//   ТелоЗапросаСтрока - Строка - 
//   Заголовки - Соответствие - 
Процедура ОтправитьЗапрос(Знач Адрес, Знач ТелоЗапросаСтрока, Знач Заголовки, Знач ЛиПотоковый = Ложь, Знач ЛиПроверочный = Ложь, знач МинИнтервалОтветов = 500) Экспорт
	Если Не ирКэш.ДоступноБраузерWebKitЛкс() Тогда
		Возврат;
	КонецЕсли;
	мЛиПроверочный = ЛиПроверочный;
	ЗаголовкиМассив = Новый Массив;
	Для Каждого ЗаголовкиЭлемент Из Заголовки Цикл
		ЗаголовкиМассив.Добавить(Новый Структура("name,value", ЗаголовкиЭлемент.Ключ, ЗаголовкиЭлемент.Значение));
	КонецЦикла;
	ЗаголовкиТекст = ирОбщий.ОбъектВТекстЖСОНЛкс(ЗаголовкиМассив);
	мНакопленныйОтвет = "";
	мЛиПотоковый = ЛиПотоковый;
	Если мЛиПотоковый Тогда
		Браузер().sendStreamRequest(Адрес, ТелоЗапросаСтрока, ЗаголовкиТекст, МинИнтервалОтветов);
	Иначе
		Браузер().sendHttpRequest(Адрес, ТелоЗапросаСтрока, ЗаголовкиТекст);
	КонецЕсли;
КонецПроцедуры

Функция ЛиПотокЗавершен() Экспорт
	Возврат Браузер().isStreamCompleted();
КонецФункции

// Не поддерживает поток
Функция ЛиЗапросВыполняется() Экспорт
	Возврат Браузер().isActiveHttpRequest();
КонецФункции

Функция СтатусЗапроса() Экспорт
	Возврат Браузер().getRequestStatus();
КонецФункции

// Читает и удаляет полученную часть ответа
Функция ПрочитатьЧастьОтвета() Экспорт
	Возврат Браузер().getAndClearAllData();
КонецФункции

Процедура ОтменитьЗапрос() Экспорт
	Если Не ирКэш.ДоступноБраузерWebKitЛкс() Тогда
		Возврат;
	КонецЕсли;
	Браузер().cancelCurrentRequest();
КонецПроцедуры

// Отправляет запрос к ИИ через OpenAPI. Ответ приходит событием onclick
// Параметры:
//   ПараметрыЗапроса - Структура - Содержимое сообщения
//    *Текст - Строка - текст запроса
//   Форма - Форма - 
//   ОбработчикОтвета -  - 
//   БлокируемыеЭлементы -  - 
//   Кнопка - Кнопка - 
//   ЛиПроверочный - Булево - 
//   РежимНапарника - Строка - docstring, explain, review, modify, custom, system, raw
//   КлючСервиса - Строка - Ключ API для аутентификации
//   Сервер - Строка - URL-адрес API, опционально, по умолчанию - OpenRouter
//   Модель - Строка - Название модели, опционально, по умолчанию - deepseek/deepseek-chat
// Возвращаемое значение:
//   Булево - успешность запуска
Функция ОтправитьЗапросИИ(Знач ПараметрыЗапроса, Знач Форма = Неопределено, Знач ОбработчикОтвета = "", Знач БлокируемыеЭлементы = Неопределено, Знач Кнопка = Неопределено,
		Знач ЛиПотоковый = Ложь, Знач ЛиПроверочный = Ложь, Знач РежимНапарника = "custom", Знач КлючСервиса = Неопределено, Знач Сервер = Неопределено, Знач Модель = Неопределено,
		Знач ПараметрыМоделиИИ = Неопределено) Экспорт
	Если Не ирКэш.ДоступноБраузерWebKitЛкс() Тогда
		ирОбщий.СообщитьЛкс("Для отправки запроса к ИИ требуется платформа 8.3.14+");
		Возврат Ложь;
	КонецЕсли;
	мПлатформа = ирКэш.Получить(); // Для сервера
	Если ПустаяСтрока(КлючСервиса) Тогда
		КлючСервиса = мПлатформа.КлючСервисаИИ();
	КонецЕсли;
	Сервер = мПлатформа.СерверИИ(, Сервер);
	Модель = мПлатформа.МодельИИ(, Модель, Сервер);
	Если ПустаяСтрока(КлючСервиса) Тогда
		Если Не ЛиПроверочный Тогда
			#Если Клиент Тогда
				Форма = ирКлиент.ОткрытьНастройкиПоляПрограммыЛкс("", Истина, "КлючСервисаИИ");
			#КонецЕсли
		КонецЕсли;
		ВызватьИсключение "Не указан ключ сервиса ИИ";
	КонецЕсли; 
	Если мОписаниеЗадания <> Неопределено Тогда
		ирКлиент.ОтменитьЗаданиеФормыЛкс(Форма, мОписаниеЗадания);
		ОтменитьЗапрос();
		мОписаниеЗадания = Неопределено;
		Возврат Ложь;
	КонецЕсли;
	мОписаниеЗадания = ирКлиент.ОписаниеФоновогоЗаданияФормыЛкс("ЗапросИИ",, ПараметрыЗапроса,, Кнопка, ОбработчикОтвета,,, БлокируемыеЭлементы,,,,, Ложь);
	ирКлиент.ЗапуститьИлиОтменитьФоновоеЗаданиеФормыЛкс(Форма, мОписаниеЗадания);
	Если Сервер = "https://code.1c.ai" Тогда
		//Если РежимНапарника = "custom" Тогда
			ПараметрыЧатаНапарника = Новый Структура;
			ПараметрыЧатаНапарника.Вставить("instruction", ПараметрыЗапроса.Текст);
			ПараметрыЧатаНапарника.Вставить("tools");
		//КонецЕсли;
		ПолеТекстаПрограммы = ирКэш.ПолеТекстаПрограммы();
		Соединение = ПолеТекстаПрограммы.СоединениеНапарника();
		Если ТипЗнч(Соединение) = Тип("Строка") Тогда
			ирОбщий.СообщитьЛкс(ирОбщий.СтрШаблонЛкс("Ошибка соединения с Напарником: %1", Соединение));
			Возврат Ложь;
		КонецЕсли;
		Запрос = ирОбщий.ЗапросHTTPЛкс("/chat_api/v1/conversations/");
		СтруктураЗапроса = Новый Структура;
		//"tool_name": "review",
		//"ui_language": "russian",
		//"programming_language": "1с",
		//"script_language": "Russian"
		СтруктураЗапроса.Вставить("tool_name", РежимНапарника); 
		СтруктураЗапроса.Вставить("skill_name", РежимНапарника); 
		СтруктураЗапроса.Вставить("is_chat", Истина); 
		СтруктураЗапроса.Вставить("programming_language", "1с"); 
		СтруктураЗапроса.Вставить("ui_language", "russian"); 
		СтруктураЗапроса.Вставить("script_language", ПолеТекстаПрограммы.ИмяЯзыкаЧеловека()); 
		Заголовки = ПолеТекстаПрограммы.ЗаголовкиЗапросаНапарника(КлючСервиса);
		Запрос.УстановитьТелоИзСтроки(ирОбщий.ОбъектВТекстЖСОНЛкс(СтруктураЗапроса));
		Запрос.Заголовки = Заголовки;
		Ответ = Соединение.ОтправитьДляОбработки(Запрос);
		Если Не ирОбщий.ЛиУспешныйОтветHTTPЛкс(Ответ) Тогда
			ирОбщий.СообщитьЛкс(ирОбщий.СтрШаблонЛкс("Ошибка %1: %2", Ответ.КодСостояния, Ответ.ПолучитьТелоКакСтроку()));
			ирКлиент.ОтменитьЗаданиеФормыЛкс(Форма, мОписаниеЗадания);
			мОписаниеЗадания = Неопределено;
			Возврат Ложь;
		КонецЕсли;
		СтруктураОтвета = ирОбщий.ОбъектИзТекстаЖСОНЛкс(Ответ.ПолучитьТелоКакСтроку()); // см. ПрочитатьJSON(ОбразецОтветаСозданияЧатаНапарника())
		Адрес = "https://code.1c.ai/chat_api/v1/conversations/" + СтруктураОтвета.uuid + "/messages";
		СтруктураЗапроса = Новый Структура;
		//"tool_content": ...
		СтруктураЗапроса.Вставить("role", "user"); 
		СтруктураЗапроса.Вставить("parent_uuid", );
		//СтруктураЗапроса.Вставить("tool_content", ПараметрыЗапроса);
		СтруктураЗапроса.Вставить("content", Новый Структура("content", ПараметрыЧатаНапарника));
	Иначе
		Заголовки = Новый Соответствие;
		Заголовки.Вставить("Content-Type", "application/json"); 
		//Заголовки.Вставить("Origin", "https://chat.deepseek.com");
		//Заголовки.Вставить("Referer", "https://chat.deepseek.com/");
		Если ЗначениеЗаполнено(КлючСервиса) Тогда
			Заголовки.Вставить("Authorization", "Bearer " + КлючСервиса);
		КонецЕсли;
		СтруктураЗапроса = Новый Структура;
		СтруктураЗапроса.Вставить("model", Модель);
		СтруктураЗапроса.Вставить("messages", Новый Массив);
		СтруктураЗапроса.Вставить("stream", ЛиПотоковый);
		Если ПараметрыМоделиИИ = Неопределено Тогда
			ПараметрыМоделиИИ = мПлатформа.ПараметрыМоделиИИ();
		КонецЕсли;
		Для Каждого ЭлементСписка Из ПараметрыМоделиИИ Цикл
			Если Не ЭлементСписка.Пометка Тогда
				Продолжить;
			КонецЕсли;
			Если Не ирОбщий.ЛиИмяПеременнойЛкс(ЭлементСписка.Представление) Тогда
				ирКлиент.ОткрытьНастройкиПоляПрограммыЛкс("", Истина, "ПараметрыМоделиИИ");
				ВызватьИсключение ирОбщий.СтрШаблонЛкс("Неверное имя параметра модели ИИ: %1", ЭлементСписка.Представление);
			КонецЕсли;
			ЗначениеПараметра = ЭлементСписка.Значение;
			Если ЭлементСписка.Представление = "reasoning" Тогда
				ЗначениеПараметра = Новый Структура("enable", ЗначениеПараметра);
			КонецЕсли;
			СтруктураЗапроса.Вставить(ЭлементСписка.Представление, ЗначениеПараметра); 
		КонецЦикла;
		СтруктураЗапроса.messages.Добавить(Новый Структура("role,content", "user", ПараметрыЗапроса.Текст));
		Адрес = Сервер + "/v1/chat/completions";
	КонецЕсли;
	ОтправитьЗапрос(Адрес, ирОбщий.ОбъектВТекстЖСОНЛкс(СтруктураЗапроса), Заголовки, ЛиПотоковый, ЛиПроверочный);
	Возврат Истина;
КонецФункции

Функция ОбразецОтветаСозданияЧатаНапарника() Экспорт
	Возврат "{""uuid"":""028c6d0f-864f-4b2d-846b-6c8b161c4171""}";
КонецФункции

Функция ОбразецЧатЗапросНапарника() Экспорт
	Возврат "{
	|  ""parent_uuid"": null,
	|  ""tool_content"": {
	|    ""instruction"": ""напиши код расчета суммы по документу Документ1"",
	|    ""code"": [
	|      {
	|        ""content"": ""\r\n\r\nФункция СуммаМоя(Скидка) Экспорт\r\n\t\r\nКонецФункци"",
	|        ""path"": ""Тест_83_серверная/src/Documents/Документ1/ObjectModule.bsl"",
	|        ""path_formatted"": ""Документы→Документ1→Модуль объекта"",
	|        ""start_line"": 4,
	|        ""end_line"": 8
	|      }
	|    ]
	|  }
	|}"
КонецФункции

Функция ОбразецЧатОтветНапарника() Экспорт
	Возврат "data: {""uuid"":""c197b124-e46e-4692-84b4-c4cd574c2f45:c6b3de57"",""role"":""user"",""create_time"":""2025-10-11T08:08:32.452288"",""finished"":false}
	|
	|data: {""uuid"":""c197b124-e46e-4692-84b4-c4cd574c2f45:c6b3de57"",""role"":""user"",""create_time"":""2025-10-11T08:08:32.452288"",""finished"":true}
	|
	|data: {""uuid"":""c197b124-e46e-4692-84b4-c4cd574c2f45:8ad69bf4"",""role"":""assistant"",""content"":{""text"":""""},""parent_uuid"":""c197b124-e46e-4692-84b4-c4cd574c2f45:c6b3de57"",""create_time"":""2025-10-11T08:08:32.477925"",""finished"":false}
	|
	|data: {""uuid"":""c197b124-e46e-4692-84b4-c4cd574c2f45:8ad69bf4"",""role"":""assistant"",""content"":{""text"":"" Для расчета суммы по документу \""Документ1\"" нам необходимо знать структуру этого документа. Предпо""},""parent_uuid"":""c197b124-e46e-4692-84b4-c4cd574c2f45:c6b3de57"",""create_time"":""2025-10-11T08:08:32.477925"",""finished"":false}
	|
	|data: {""uuid"":""c197b124-e46e-4692-84b4-c4cd574c2f45:8ad69bf4"",""role"":""assistant"",""content"":{""text"":"" Для расчета суммы по документу \""Документ1\"" нам необходимо знать структуру этого документа. Предположим, что документ имеет табличную часть \""Товары\"" с колонками \""Количество\"" и \""Цена\"".""},""parent_uuid"":""c197b124-e46e-4692-84b4-c4cd574c2f45:c6b3de57"",""create_time"":""2025-10-11T08:08:32.477925"",""finished"":false}
	|
	|data: {""uuid"":""c197b124-e46e-4692-84b4-c4cd574c2f45:8ad69bf4"",""role"":""assistant"",""content"":{""text"":"" Для расчета суммы по документу \""Документ1\"" нам необходимо знать структуру этого документа. Предположим, что документ имеет табличную часть \""Товары\"" с колонками \""Количество\"" и \""Цена\"". Тогда функция расчета суммы с учетом скидки может выглядеть следующим образом:\n\n```1с\nФункция Су""},""parent_uuid"":""c197b124-e46e-4692-84b4-c4cd574c2f45:c6b3de57"",""create_time"":""2025-10-11T08:08:32.477925"",""finished"":false}
	|
	|"
КонецФункции

Функция ОбработатьОтвет(Форма) Экспорт
	Если Ложь
		Или мОписаниеЗадания = Неопределено 
		Или мОписаниеЗадания.Состояние = СостояниеФоновогоЗадания.Отменено
	Тогда
		мОписаниеЗадания = Неопределено;
		Возврат "";
	КонецЕсли;
	ТелоОтвета = СокрЛ(ПрочитатьЧастьОтвета());
	ЛиПотокЗавершен = Не мЛиПотоковый Или ЛиПотокЗавершен();
	ПрефиксЧастиОтвета = "data: ";
	Если ирОбщий.СтрНачинаетсяСЛкс(ТелоОтвета, "Ошибка:") Тогда
		мОписаниеЗадания.Состояние = СостояниеФоновогоЗадания.ЗавершеноАварийно;
	Иначе
		Если ирОбщий.СтрНачинаетсяСЛкс(ТелоОтвета, "Отмена:") Тогда
			// Только для потокового режима
		ИначеЕсли ирОбщий.СтрНачинаетсяСЛкс(ТелоОтвета, "Конец:") Тогда
			// Только для потокового режима
			ТелоОтвета = "";
		ИначеЕсли Ложь
			Или ирОбщий.СтрНачинаетсяСЛкс(ТелоОтвета, ПрефиксЧастиОтвета) 
			Или Найти(ТелоОтвета, Символы.ПС + ПрефиксЧастиОтвета)
		Тогда
			ТекстовыйДокумент = Новый ТекстовыйДокумент;
			ТекстовыйДокумент.УстановитьТекст(ТелоОтвета);
			Результат = Новый Массив;
			Для НомерСтроки = 1 По ТекстовыйДокумент.КоличествоСтрок() Цикл
				СтрокаТекста = ТекстовыйДокумент.ПолучитьСтроку(НомерСтроки);
				Если ирОбщий.СтрНачинаетсяСЛкс(СтрокаТекста, ПрефиксЧастиОтвета) Тогда
					ТекстБезПервогоФрагмента = ирОбщий.ТекстБезПервогоФрагментаЛкс(СтрокаТекста, ПрефиксЧастиОтвета);
					Если ТекстБезПервогоФрагмента = "[DONE]" Тогда
						// openrouter.ai
						ЛиПотокЗавершен = Истина;
						Прервать;
					КонецЕсли;
					ЭлементСообщения = ирОбщий.ОбъектИзТекстаЖСОНЛкс(ТекстБезПервогоФрагмента);
					Если ирОбщий.СвойствоСтруктурыЛкс(ЭлементСообщения, "content_delta") <> Неопределено Тогда
						// Напарник
						Результат.Добавить(ЭлементСообщения.content_delta.content);
					ИначеЕсли ирОбщий.СвойствоСтруктурыЛкс(ЭлементСообщения, "content") <> Неопределено Тогда
						// Напарник
						Если ирОбщий.СвойствоСтруктурыЛкс(ЭлементСообщения, "finished") = Истина Тогда
							ЛиПотокЗавершен = Истина;
						КонецЕсли;
					ИначеЕсли ЭлементСообщения.Свойство("choices") Тогда
						// openrouter.ai
						ВложеннаяЧасть = ЭлементСообщения.choices[0].delta;
						Если ВложеннаяЧасть.Свойство("content") Тогда
							ЧастьОтвета = ВложеннаяЧасть.content;
							//ЧастьОтвета = ВложеннаяЧасть.reasoning;
							Результат.Добавить(ЧастьОтвета);
						КонецЕсли;
					КонецЕсли;
				КонецЕсли;
			КонецЦикла;  
			ТелоОтвета = ирОбщий.СтрСоединитьЛкс(Результат, "");
			мНакопленныйОтвет = мНакопленныйОтвет + ТелоОтвета;
		Иначе	
			Если ирОбщий.ЛиТекстJSONЛкс(ТелоОтвета) Тогда
				ОтветЖСОН = ирОбщий.ОбъектИзТекстаЖСОНЛкс(ТелоОтвета);
				Если Истина
					И ОтветЖСОН.Свойство("choices")
					И ОтветЖСОН.choices.Количество() > 0 
				Тогда
					// сайта openrouter.ai
					мНакопленныйОтвет = ОтветЖСОН.choices[0].message.content; // Строка
				Иначе
					мНакопленныйОтвет = "";
				КонецЕсли;
			КонецЕсли;
		КонецЕсли; 
		Если ЛиПотокЗавершен Тогда
			мОписаниеЗадания.Состояние = СостояниеФоновогоЗадания.Завершено;
		КонецЕсли;
	КонецЕсли;
	мОписаниеЗадания.Параметры.Вставить("Ответ", мНакопленныйОтвет);
	Если ЛиПотокЗавершен Тогда
		Если ПустаяСтрока(мНакопленныйОтвет) Тогда
			мОписаниеЗадания.Параметры = Неопределено;
		КонецЕсли;
		ирКлиент.ОбработатьЗавершениеЗаданияФормыЛкс(мОписаниеЗадания, Форма,,,, мОписаниеЗадания.Параметры); 
		Длительность = ТекущаяДата() - мОписаниеЗадания.НачалоВыполнения;
		Если Истина
			И Не мЛиПотоковый
			И (Ложь
				Или ирОбщий.СвойствоСтруктурыЛкс(мОписаниеЗадания.Параметры, "проверка") = Истина
				Или Длительность > 5 
				Или мЛиПроверочный И Длительность > 1)
		Тогда
			ирОбщий.СообщитьЛкс("ИИ ответил за " + Длительность + " секунд",,, Истина, Ложь);
		КонецЕсли;
		Если мОписаниеЗадания.Состояние = СостояниеФоновогоЗадания.ЗавершеноАварийно Тогда
			ирОбщий.СообщитьЛкс(ТелоОтвета);
		КонецЕсли;
		мОписаниеЗадания = Неопределено;
	Иначе
		Если мОписаниеЗадания.ОбработчикЗавершения <> "" Тогда
			Состояние = мОписаниеЗадания.Состояние;
			Параметры = мОписаниеЗадания.Параметры;
			Выполнить("Форма." + мОписаниеЗадания.ОбработчикЗавершения + "(Состояние, Параметры)");
		КонецЕсли;
	КонецЕсли;
	Возврат мНакопленныйОтвет;
КонецФункции

//ирПортативный лФайл = Новый Файл(ИспользуемоеИмяФайла);
//ирПортативный ПолноеИмяФайлаБазовогоМодуля = Лев(лФайл.Путь, СтрДлина(лФайл.Путь) - СтрДлина("Модули\")) + "ирПортативный.epf";
//ирПортативный #Если Клиент Тогда
//ирПортативный 	Контейнер = Новый Структура();
//ирПортативный 	Оповестить("ирПолучитьБазовуюФорму", Контейнер);
//ирПортативный 	Если Не Контейнер.Свойство("ирПортативный", ирПортативный) Тогда
//ирПортативный 		ирПортативный = ВнешниеОбработки.ПолучитьФорму(ПолноеИмяФайлаБазовогоМодуля);
//ирПортативный 		ирПортативный.Открыть();
//ирПортативный 	КонецЕсли; 
//ирПортативный #Иначе
//ирПортативный 	ирПортативный = ВнешниеОбработки.Создать(ПолноеИмяФайлаБазовогоМодуля, Ложь); // Это будет второй экземпляр объекта
//ирПортативный #КонецЕсли
//ирПортативный ирОбщий = ирПортативный.ОбщийМодульЛкс("ирОбщий");
//ирПортативный ирКэш = ирПортативный.ОбщийМодульЛкс("ирКэш");
//ирПортативный ирСервер = ирПортативный.ОбщийМодульЛкс("ирСервер");
//ирПортативный ирКлиент = ирПортативный.ОбщийМодульЛкс("ирКлиент");

#КонецЕсли
