/
psklyarov
/
agent-replacer
Обзор
Документация
Войти
/
psklyarov
/
agent-replacer
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
master
src/main/java/com/example/agentreplacer/chat/ChatService.java
402 строки
21 KB
pavelsklyarov
insecure ssl
16 дек 2025, 09:03
16 дек 2025, 09:03
da61b1f
Код
Авторство
О чём код?
package com.example.agentreplacer.chat; import com.example.agentreplacer.plan.ChangePlan; import com.example.agentreplacer.plan.PlanService; import com.example.agentreplacer.jira.JiraService; import com.example.agentreplacer.bitbucket.BitbucketProperties; import org.springframework.stereotype.Service; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.fasterxml.jackson.databind.ObjectMapper; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; @Service public class ChatService { private final Map<String, ChatSessionState> sessions = new ConcurrentHashMap<>(); private final PlanService planService; private final JiraService jiraService; private final BitbucketProperties bitbucketProperties; private final java.nio.file.Path sessionsDir; private static final Logger log = LoggerFactory.getLogger(ChatService.class); private final ObjectMapper objectMapper = new ObjectMapper(); public ChatService(PlanService planService, JiraService jiraService, BitbucketProperties bitbucketProperties) { this.planService = planService; this.jiraService = jiraService; this.bitbucketProperties = bitbucketProperties; String ws = bitbucketProperties.getWorkspaceDir() == null ? "./workspace" : bitbucketProperties.getWorkspaceDir(); this.sessionsDir = java.nio.file.Path.of(ws, "sessions"); try { java.nio.file.Files.createDirectories(this.sessionsDir); } catch (Exception ignored) { } } public ChatSessionState createSession() { ChatSessionState session = new ChatSessionState(); session.getMessages().add(new ChatMessage("assistant", "Привет! Я помогу массово изменить код в репозиториях Bitbucket.\n" + "Давай по порядку. 1/8: Какие репозитории нужно ВКЛЮЧИТЬ в обработку? " + "(Перечисли названия/паттерны; если все — напиши: все)")); sessions.put(session.getSessionId(), session); log.info("New session created: {}", session.getSessionId()); return session; } public ChatSessionState getSession(String sessionId) { return sessions.get(sessionId); } public ChatSessionState getOrRestore(String sessionId) { ChatSessionState s = sessions.get(sessionId); if (s != null) return s; // пробуем восстановить план с диска try { ChangePlan plan = loadPlanFromDisk(sessionId); if (plan != null) { ChatSessionState restored = new ChatSessionState(); restored.setSessionId(sessionId); restored.getMessages().add(new ChatMessage("assistant", "Сессия восстановлена по ID. План загружен с диска.")); restored.setPlan(plan); sessions.put(sessionId, restored); return restored; } } catch (Exception ignored) { } return null; } public ChatMessage handleUserMessage(String sessionId, String userMessage) { ChatSessionState session = sessions.get(sessionId); if (session == null) { log.warn("handleUserMessage: session not found: {}", sessionId); throw new IllegalArgumentException("Сессия не найдена"); } String msg = userMessage == null ? "" : userMessage.trim(); log.info("Session {} user message: {}", sessionId, msg); session.getMessages().add(new ChatMessage("user", userMessage)); // Команды исправления плана: "исправить <ключевое_слово>" if (msg.toLowerCase().startsWith("исправить")) { String[] parts = msg.toLowerCase().split("\\s+", 2); String key = parts.length > 1 ? parts[1].trim() : ""; if (!key.isEmpty()) { String question = resetFieldAndGetQuestion(session, key); if (question != null) { session.setPlan(null); // сбрасываем план для пересборки return pushAssistant(session, question); } else { return pushAssistant(session, "Не понял, что нужно исправить. Доступно: исключения | include | exclude | изменения | проект | ветки | режим | задача."); } } else { return pushAssistant(session, "Укажи, что исправить: исключения | include | exclude | изменения | проект | ветки | режим | задача."); } } // 1/8: включаемые репозитории if (session.getIncludedReposAnswer() == null) { if (msg.isBlank()) { return pushAssistant(session, "Не получил список включаемых репозиториев. Укажи названия/паттерны или напиши: все."); } session.setIncludedReposAnswer(msg); log.debug("Session {} set includedReposAnswer", sessionId); if (getSession(sessionId).hasAllAnswers()) { return rebuildPlanAndAnnounce(sessionId, session); } return pushAssistant(session, "2/8: Какие репозитории нужно ИСКЛЮЧИТЬ из проверки? (названия/паттерны или напиши: нет)"); } if (session.getExcludedReposAnswer() == null) { if (msg.isBlank()) { return pushAssistant(session, "Не получил список исключаемых репозиториев. Укажи названия/паттерны или напиши: нет."); } session.setExcludedReposAnswer(msg); log.debug("Session {} set excludedReposAnswer", sessionId); if (getSession(sessionId).hasAllAnswers()) { return rebuildPlanAndAnnounce(sessionId, session); } return pushAssistant(session, "3/8: Как определить, что файл нужно менять? (опиши имя файла или условия по содержимому)"); } if (session.getIncludeCriteriaAnswer() == null) { if (msg.isBlank()) { return pushAssistant(session, "Не получил критерии включения. Опиши имя файла (regex) или условия по содержимому."); } session.setIncludeCriteriaAnswer(msg); log.debug("Session {} set includeCriteriaAnswer", sessionId); if (getSession(sessionId).hasAllAnswers()) { return rebuildPlanAndAnnounce(sessionId, session); } return pushAssistant(session, "4/8: Как определить, что файл не нужно менять? (опиши имя файла или условия по содержимому, или напиши: нет)"); } if (session.getExcludeCriteriaAnswer() == null) { if (msg.isBlank()) { return pushAssistant(session, "Не получил критерии исключения. Опиши имя файла (regex) или условия по содержимому, или напиши: нет."); } session.setExcludeCriteriaAnswer(msg); log.debug("Session {} set excludeCriteriaAnswer", sessionId); if (getSession(sessionId).hasAllAnswers()) { return rebuildPlanAndAnnounce(sessionId, session); } return pushAssistant(session, "5/8: Что конкретно нужно менять? (если простая замена строки/регекспом — опиши \"что\" на \"что\")"); } if (session.getWhatToChangeAnswer() == null) { if (msg.isBlank()) { return pushAssistant(session, "Не получил описание изменений. Укажи замены (что→на что) или инструкции."); } session.setWhatToChangeAnswer(msg); log.debug("Session {} set whatToChangeAnswer", sessionId); if (getSession(sessionId).hasAllAnswers()) { return rebuildPlanAndAnnounce(sessionId, session); } return pushAssistant(session, "6/8: Укажи ключ проекта Bitbucket (например, DEMO)."); } if (session.getProjectKeyAnswer() == null) { if (msg.isBlank()) { return pushAssistant(session, "Не получил ключ проекта. Пример: DEMO."); } session.setProjectKeyAnswer(msg); log.debug("Session {} set projectKeyAnswer", sessionId); if (getSession(sessionId).hasAllAnswers()) { return rebuildPlanAndAnnounce(sessionId, session); } return pushAssistant(session, "7/8: В каких ветках вносить изменения? (например: develop, release/* — можно перечислить через запятую)"); } if (session.getTargetBranchesAnswer() == null) { if (msg.isBlank()) { return pushAssistant(session, "Не получил список веток. Пример: develop, release/2025.12"); } session.setTargetBranchesAnswer(msg); log.debug("Session {} set targetBranchesAnswer", sessionId); if (getSession(sessionId).hasAllAnswers()) { return rebuildPlanAndAnnounce(sessionId, session); } return pushAssistant(session, "8/8: Выбери режим работы: FEATURE_PR (feature-ветка + PR), DIRECT_COMMIT (прямой коммит), CONFIRM (подтверждение в чате перед коммитом)."); } if (session.getModeAnswer() == null) { String mode = msg.toUpperCase(); switch (mode) { case "FEATURE_PR", "DIRECT_COMMIT", "CONFIRM" -> { session.setModeAnswer(mode); log.debug("Session {} set modeAnswer", sessionId); if (getSession(sessionId).hasAllAnswers()) { return rebuildPlanAndAnnounce(sessionId, session); } return pushAssistant(session, "8/8: Укажи номер задачи (например, ABC-123), под которым нужно сделать коммит и ветку."); } default -> { return pushAssistant(session, "Неверный режим. Допустимо: FEATURE_PR, DIRECT_COMMIT, CONFIRM. Укажи режим."); } } } if (session.getTaskNumberAnswer() == null) { if (msg.isBlank()) { return pushAssistant(session, "Не получил номер задачи. Пример: ABC-123"); } // Валидация номера задачи в Jira (если настроена) try { if (jiraService.isConfigured()) { boolean exists = jiraService.issueExists(msg); if (!exists) { return pushAssistant(session, "Задача " + msg + " не найдена в Jira. Проверь номер и укажи корректное значение."); } } else { addAssistantMessage(sessionId, "Внимание: Jira не сконфигурирована (jira.base-url). Пропускаю проверку номера задачи."); } } catch (Exception e) { return pushAssistant(session, "Не удалось проверить задачу в Jira: " + e.getMessage() + ". Укажи номер задачи ещё раз или настрой Jira и повтори."); } session.setTaskNumberAnswer(msg); // Сформировать план через LLM log.info("Session {} building plan via LLM", sessionId); try { var payload = new java.util.LinkedHashMap<String, Object>(); payload.put("excludedReposAnswer", session.getExcludedReposAnswer()); payload.put("includeCriteriaAnswer", session.getIncludeCriteriaAnswer()); payload.put("excludeCriteriaAnswer", session.getExcludeCriteriaAnswer()); payload.put("whatToChangeAnswer", session.getWhatToChangeAnswer()); payload.put("projectKeyAnswer", session.getProjectKeyAnswer()); payload.put("targetBranchesAnswer", session.getTargetBranchesAnswer()); payload.put("modeAnswer", session.getModeAnswer()); payload.put("taskNumberAnswer", session.getTaskNumberAnswer()); String inputJson = objectMapper.writeValueAsString(payload); addAssistantMessage(sessionId, "LLM план: входные данные:\n" + truncate(inputJson, 4000)); } catch (Exception ignored) { } String raw = null; try { raw = planService.buildPlanRawJson(session); addAssistantMessage(sessionId, "LLM план: сырой ответ:\n" + truncate(raw, 4000)); } catch (Exception e) { addAssistantMessage(sessionId, "LLM план: не удалось получить сырой ответ: " + e.getMessage()); } ChangePlan plan = null; try { plan = planService.buildPlan(session); } catch (Exception e) { addAssistantMessage(sessionId, "LLM план: ошибка разбора плана: " + e.getMessage()); throw e; } session.setPlan(plan); savePlanToDisk(sessionId, plan); try { String planJson = objectMapper.writeValueAsString(plan); addAssistantMessage(sessionId, "LLM план: ответ:\n" + truncate(planJson, 4000)); } catch (Exception ignored) { } log.info("Session {} plan ready: projectKey={}, mode={}, targets={}", sessionId, plan.getProjectKey(), plan.getMode(), plan.getTargetBranches()); return pushAssistant(session, "Отлично, план сформирован. Можешь просмотреть и запустить изменение.\n" + "Если нужно что-то уточнить — напиши \"исправить <ключевое слово>\", где ключевые слова: исключения | include | exclude | изменения | проект | ветки | режим | задача.\n" + "Скажи: \"Запусти\", чтобы начать обработку репозиториев."); } if ("запусти".equalsIgnoreCase(msg)) { log.info("Session {} received start command", sessionId); if (!session.hasAllAnswers()) { // Спросим следующий обязательный вопрос String q = nextRequiredQuestion(session); if (q != null) return pushAssistant(session, q); return pushAssistant(session, "Не все ответы получены. Продолжим вопросы по порядку."); } return pushAssistant(session, "Принято. Для запуска используй кнопку в интерфейсе или дорожку API /api/run."); } return pushAssistant(session, "Я запомнил. Можешь уточнить детали или скажи \"Запусти\" чтобы начать."); } private ChatMessage pushAssistant(ChatSessionState session, String content) { ChatMessage msg = new ChatMessage("assistant", content); session.getMessages().add(msg); return msg; } public void addAssistantMessage(String sessionId, String content) { ChatSessionState session = sessions.get(sessionId); if (session == null) return; pushAssistant(session, content); } private String truncate(String s, int max) { if (s == null) return null; if (s.length() <= max) return s; return s.substring(0, max) + "\n... (truncated, total " + s.length() + " chars)"; } private ChatMessage rebuildPlanAndAnnounce(String sessionId, ChatSessionState session) { addAssistantMessage(sessionId, "Обновляю план на основе уточнённых ответов…"); try { // Показать входные данные var payload = new java.util.LinkedHashMap<String, Object>(); payload.put("includedReposAnswer", session.getIncludedReposAnswer()); payload.put("excludedReposAnswer", session.getExcludedReposAnswer()); payload.put("includeCriteriaAnswer", session.getIncludeCriteriaAnswer()); payload.put("excludeCriteriaAnswer", session.getExcludeCriteriaAnswer()); payload.put("whatToChangeAnswer", session.getWhatToChangeAnswer()); payload.put("projectKeyAnswer", session.getProjectKeyAnswer()); payload.put("targetBranchesAnswer", session.getTargetBranchesAnswer()); payload.put("modeAnswer", session.getModeAnswer()); payload.put("taskNumberAnswer", session.getTaskNumberAnswer()); String inputJson = objectMapper.writeValueAsString(payload); addAssistantMessage(sessionId, "LLM план: входные данные:\n" + truncate(inputJson, 4000)); } catch (Exception ignored) { } try { String raw = planService.buildPlanRawJson(session); addAssistantMessage(sessionId, "LLM план: сырой ответ:\n" + truncate(raw, 4000)); } catch (Exception e) { addAssistantMessage(sessionId, "LLM план: не удалось получить сырой ответ: " + e.getMessage()); } try { ChangePlan plan = planService.buildPlan(session); session.setPlan(plan); savePlanToDisk(sessionId, plan); String planJson = objectMapper.writeValueAsString(plan); addAssistantMessage(sessionId, "LLM план: ответ:\n" + truncate(planJson, 4000)); } catch (Exception e) { addAssistantMessage(sessionId, "LLM план: ошибка разбора плана: " + e.getMessage()); } return pushAssistant(session, "План обновлён. Скажи \"Запусти\", чтобы начать обработку."); } private String resetFieldAndGetQuestion(ChatSessionState session, String key) { switch (key) { case "включение-репозиториев", "repos-include", "include-repos"-> { session.setIncludedReposAnswer(null); return "1/8: Какие репозитории нужно ВКЛЮЧИТЬ в обработку? (паттерны; или напиши: все)"; } case "исключения", "exclude-repos", "repo-exclude", "repos" -> { session.setExcludedReposAnswer(null); return "2/8: Какие репозитории нужно ИСКЛЮЧИТЬ из проверки? (названия/паттерны или напиши: нет)"; } case "include", "включение" -> { session.setIncludeCriteriaAnswer(null); return "3/8: Как определить, что файл нужно менять? (имя файла или условия по содержимому)"; } case "exclude", "исключение" -> { session.setExcludeCriteriaAnswer(null); return "4/8: Как определить, что файл не нужно менять? (имя файла или условия по содержимому, или напиши: нет)"; } case "изменения", "что", "что менять", "changes" -> { session.setWhatToChangeAnswer(null); return "5/8: Что конкретно нужно менять? (замены \"что\" на \"что\" или инструкции)"; } case "проект", "project" -> { session.setProjectKeyAnswer(null); return "6/8: Укажи ключ проекта Bitbucket (например, DEMO)."; } case "ветки", "branches" -> { session.setTargetBranchesAnswer(null); return "7/8: В каких ветках вносить изменения? (например: develop, release/* — через запятую)"; } case "режим", "mode" -> { session.setModeAnswer(null); return "8/8: Выбери режим: FEATURE_PR | DIRECT_COMMIT | CONFIRM."; } case "задача", "task" -> { session.setTaskNumberAnswer(null); return "8/8: Укажи номер задачи (например, ABC-123)."; } default -> { return null; } } } private String nextRequiredQuestion(ChatSessionState s) { if (s.getIncludedReposAnswer() == null) return "1/8: Какие репозитории нужно ВКЛЮЧИТЬ в обработку? (паттерны; или напиши: все)"; if (s.getExcludedReposAnswer() == null) return "2/8: Какие репозитории нужно ИСКЛЮЧИТЬ из проверки? (названия/паттерны или напиши: нет)"; if (s.getIncludeCriteriaAnswer() == null) return "3/8: Как определить, что файл нужно менять? (имя файла или условия по содержимому)"; if (s.getExcludeCriteriaAnswer() == null) return "4/8: Как определить, что файл не нужно менять? (имя файла или условия по содержимому, или напиши: нет)"; if (s.getWhatToChangeAnswer() == null) return "5/8: Что конкретно нужно менять? (замены \"что\" на \"что\" или инструкции)"; if (s.getProjectKeyAnswer() == null) return "6/8: Укажи ключ проекта Bitbucket (например, DEMO)."; if (s.getTargetBranchesAnswer() == null) return "7/8: В каких ветках вносить изменения? (например: develop, release/* — через запятую)"; if (s.getModeAnswer() == null) return "8/8: Выбери режим: FEATURE_PR | DIRECT_COMMIT | CONFIRM."; if (s.getTaskNumberAnswer() == null) return "8/8: Укажи номер задачи (например, ABC-123)."; return null; } private java.nio.file.Path planFilePath(String sessionId) { return sessionsDir.resolve(sessionId + ".json"); } private void savePlanToDisk(String sessionId, ChangePlan plan) { try { String json = objectMapper.writeValueAsString(plan); java.nio.file.Files.writeString(planFilePath(sessionId), json, java.nio.charset.StandardCharsets.UTF_8); log.debug("Plan saved to {}", planFilePath(sessionId)); } catch (Exception e) { log.warn("Failed to save plan to disk: {}", e.getMessage()); } } private ChangePlan loadPlanFromDisk(String sessionId) { try { java.nio.file.Path p = planFilePath(sessionId); if (!java.nio.file.Files.exists(p)) return null; String json = java.nio.file.Files.readString(p, java.nio.charset.StandardCharsets.UTF_8); return objectMapper.readValue(json, ChangePlan.class); } catch (Exception e) { log.warn("Failed to load plan from disk for {}: {}", sessionId, e.getMessage()); return null; } } }