feat(aihr): add coach-hint style regeneration buttons to practice turn
- /turn accepts optional regenerate+style (serious/gentle/detailed/professional); regenerate short-circuits before rememberTraineeReply — no round advance, no trainee reply write, no scoring impact; only same-round coachHint evidence is overwritten - Style instruction injected into existing LLM turn prompt; LLM missing/failed returns original hint with explicit message (no fake data) - Mobile practice page renders 再严肃/再温柔/更细致/更专业 pills under coach hints; success replaces text with a style note, failure toasts and keeps original; old clients unaffected (fields appended)
This commit is contained in:
+3
-2
@@ -81,7 +81,7 @@ public final class AihrPracticeDto {
|
||||
public record PrepCardResponse(List<String> keyPoints, List<String> redLines, List<String> scripts, String promptVersion) {
|
||||
}
|
||||
|
||||
public record TurnRequest(String sessionId, Integer roundIndex, String traineeText, String traineeAudioUrl, Long traineeAudioOssId) {
|
||||
public record TurnRequest(String sessionId, Integer roundIndex, String traineeText, String traineeAudioUrl, Long traineeAudioOssId, Boolean regenerate, String style) {
|
||||
}
|
||||
|
||||
public record TurnResponse(
|
||||
@@ -92,7 +92,8 @@ public final class AihrPracticeDto {
|
||||
String coachHint,
|
||||
Boolean redFlag,
|
||||
Integer roundIndex,
|
||||
Boolean finished
|
||||
Boolean finished,
|
||||
String message
|
||||
) {
|
||||
}
|
||||
|
||||
|
||||
+20
-1
@@ -87,6 +87,24 @@ public class AihrPracticeLlmService {
|
||||
int defaultTrust,
|
||||
String defaultCoachHint
|
||||
) {
|
||||
return customerTurn(personaTraits, project, goal, dialogue, traineeText, nextBeat, defaultEmotion, defaultTrust, defaultCoachHint, "");
|
||||
}
|
||||
|
||||
public Optional<PracticeTurn> customerTurn(
|
||||
String personaTraits,
|
||||
String project,
|
||||
String goal,
|
||||
List<DialogueResponse> dialogue,
|
||||
String traineeText,
|
||||
String nextBeat,
|
||||
int defaultEmotion,
|
||||
int defaultTrust,
|
||||
String defaultCoachHint,
|
||||
String styleInstruction
|
||||
) {
|
||||
String styleRule = styleInstruction == null || styleInstruction.isBlank()
|
||||
? ""
|
||||
: "\n 6. 本次只调整 coachHint 风格:" + styleInstruction.trim();
|
||||
String system = """
|
||||
你只扮演物业小区业主,同时作为训练引擎给出结构化回合状态。
|
||||
人设特征:%s
|
||||
@@ -98,9 +116,10 @@ public class AihrPracticeLlmService {
|
||||
3. emotion/trust 为 0-100 整数;员工承接越好 trust 越高,推责或对抗则 redFlag=true。
|
||||
4. coachHint 给员工下一步改进建议,不超过60字。
|
||||
5. 剧情推进方向(不要照抄原句):%s
|
||||
%s
|
||||
只输出 JSON,不要输出其他文字:
|
||||
{"customerText":"","emotion":%d,"trust":%d,"coachHint":"","redFlag":false}
|
||||
""".formatted(personaTraits, project, goal, MAX_CUSTOMER_CHARS, nextBeat, defaultEmotion, defaultTrust);
|
||||
""".formatted(personaTraits, project, goal, MAX_CUSTOMER_CHARS, nextBeat, styleRule, defaultEmotion, defaultTrust);
|
||||
String user = "对话记录:\n" + renderDialogue(dialogue)
|
||||
+ "\n员工刚才说:" + traineeText
|
||||
+ "\n默认教练提示:" + defaultCoachHint
|
||||
|
||||
+77
-1
@@ -36,6 +36,7 @@ import org.dromara.aihr.domain.AihrPracticeDto.StartResponse;
|
||||
import org.dromara.aihr.domain.AihrPracticeDto.TurnRequest;
|
||||
import org.dromara.aihr.domain.AihrPracticeDto.TurnResponse;
|
||||
import org.dromara.aihr.service.AihrPracticeLlmService.PracticeTurn;
|
||||
import org.dromara.common.core.exception.ServiceException;
|
||||
import org.springframework.dao.DataAccessException;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.stereotype.Service;
|
||||
@@ -61,6 +62,12 @@ import java.util.concurrent.ConcurrentMap;
|
||||
public class AihrPracticeSeedService {
|
||||
|
||||
private static final String TENANT_ID = "000000";
|
||||
private static final Map<String, String> REGENERATE_STYLE_INSTRUCTIONS = Map.of(
|
||||
"serious", "语气更严肃、直接指出问题,明确告诉员工哪里不到位和下一句该怎么补。",
|
||||
"gentle", "语气更温柔,多用鼓励和陪伴式提醒,让员工愿意继续调整。",
|
||||
"detailed", "内容更细致,拆成更具体的动作、信息点和反馈节点。",
|
||||
"professional", "表达更专业,突出SOP依据、责任边界、留痕和闭环口径。"
|
||||
);
|
||||
private static final DateTimeFormatter TIME_FORMATTER = DateTimeFormatter.ofPattern("MM-dd HH:mm");
|
||||
|
||||
private final Map<String, ScenarioSeed> scenarios = buildScenarios();
|
||||
@@ -263,9 +270,13 @@ public class AihrPracticeSeedService {
|
||||
}
|
||||
|
||||
public TurnResponse turn(TurnRequest request) {
|
||||
String style = normalizeRegenerateStyle(request == null ? null : request.style());
|
||||
ActiveSession session = request == null || isBlank(request.sessionId()) ? null : activeSessions.get(request.sessionId());
|
||||
ScenarioSeed scenario = resolveScenario(session == null ? null : session.scenarioId(), request == null ? null : request.sessionId());
|
||||
int roundIndex = normalizeRoundIndex(request == null ? null : request.roundIndex(), scenario.rounds().size());
|
||||
if (request != null && Boolean.TRUE.equals(request.regenerate())) {
|
||||
return regenerateCoachHint(request, session, scenario, roundIndex, style);
|
||||
}
|
||||
rememberTraineeReply(request, roundIndex);
|
||||
RoundSeed currentRound = scenario.rounds().get(roundIndex);
|
||||
int nextRoundIndex = roundIndex + 1;
|
||||
@@ -285,10 +296,49 @@ public class AihrPracticeSeedService {
|
||||
coachHint,
|
||||
redFlag,
|
||||
nextRoundIndex,
|
||||
finished
|
||||
finished,
|
||||
""
|
||||
);
|
||||
}
|
||||
|
||||
private TurnResponse regenerateCoachHint(TurnRequest request, ActiveSession session, ScenarioSeed scenario, int roundIndex, String style) {
|
||||
if (isBlank(style)) {
|
||||
throw new ServiceException("请选择有效的调整风格");
|
||||
}
|
||||
RoundSeed currentRound = scenario.rounds().get(roundIndex);
|
||||
int nextRoundIndex = roundIndex + 1;
|
||||
boolean finished = nextRoundIndex >= scenario.rounds().size();
|
||||
RoundSeed nextRound = finished ? currentRound : scenario.rounds().get(nextRoundIndex);
|
||||
TurnEvidence evidence = turnEvidence(session, roundIndex);
|
||||
String originalCoachHint = evidence == null || isBlank(evidence.coachHint()) ? currentRound.coachHint() : evidence.coachHint();
|
||||
boolean redFlag = evidence != null && Boolean.TRUE.equals(evidence.redFlag());
|
||||
Integer emotion = evidence == null ? currentRound.emotion() : evidence.emotion();
|
||||
Integer trust = evidence == null ? currentRound.trust() : evidence.trust();
|
||||
String customerText = finished ? "" : customerLine(session, nextRoundIndex, nextRound.customer());
|
||||
String traineeText = traineeReply(session, roundIndex, request == null ? null : request.traineeText());
|
||||
if (session == null || isBlank(traineeText)) {
|
||||
return new TurnResponse(customerText, "", emotion, trust, originalCoachHint, redFlag, nextRoundIndex, finished, "未找到本轮员工回复,已保留原建议");
|
||||
}
|
||||
Optional<PracticeTurn> llmTurn = practiceLlmService.customerTurn(
|
||||
scenario.customer(),
|
||||
scenario.project(),
|
||||
scenario.goal(),
|
||||
dialogueTurns(session, scenario),
|
||||
traineeText,
|
||||
firstNonBlank(customerText, nextRound.customer()),
|
||||
emotion == null ? currentRound.emotion() : emotion,
|
||||
trust == null ? currentRound.trust() : trust,
|
||||
originalCoachHint,
|
||||
REGENERATE_STYLE_INSTRUCTIONS.get(style)
|
||||
);
|
||||
if (llmTurn.isEmpty() || isBlank(llmTurn.get().coachHint())) {
|
||||
return new TurnResponse(customerText, "", emotion, trust, originalCoachHint, redFlag, nextRoundIndex, finished, "教练建议重生成失败,已保留原建议");
|
||||
}
|
||||
String coachHint = redFlag ? redFlagCoachHint(llmTurn.get().coachHint()) : llmTurn.get().coachHint();
|
||||
rememberTurnEvidence(session, roundIndex, emotion, trust, redFlag, coachHint);
|
||||
return new TurnResponse(customerText, "", emotion, trust, coachHint, redFlag, nextRoundIndex, finished, "");
|
||||
}
|
||||
|
||||
private boolean hasRedFlag(String traineeText) {
|
||||
if (isBlank(traineeText)) {
|
||||
return false;
|
||||
@@ -1510,6 +1560,32 @@ public class AihrPracticeSeedService {
|
||||
}
|
||||
}
|
||||
|
||||
private String normalizeRegenerateStyle(String style) {
|
||||
if (isBlank(style)) {
|
||||
return "";
|
||||
}
|
||||
String safeStyle = style.trim();
|
||||
if (!REGENERATE_STYLE_INSTRUCTIONS.containsKey(safeStyle)) {
|
||||
throw new ServiceException("style 仅支持 serious/gentle/detailed/professional");
|
||||
}
|
||||
return safeStyle;
|
||||
}
|
||||
|
||||
private String traineeReply(ActiveSession session, int roundIndex, String fallback) {
|
||||
List<String> replies = snapshot(session == null ? null : session.traineeReplies());
|
||||
return roundIndex < replies.size() ? firstNonBlank(replies.get(roundIndex), firstNonBlank(fallback, "")) : firstNonBlank(fallback, "");
|
||||
}
|
||||
|
||||
private String customerLine(ActiveSession session, int roundIndex, String fallback) {
|
||||
List<String> lines = snapshot(session == null ? null : session.customerLines());
|
||||
return roundIndex < lines.size() ? firstNonBlank(lines.get(roundIndex), fallback) : fallback;
|
||||
}
|
||||
|
||||
private TurnEvidence turnEvidence(ActiveSession session, int roundIndex) {
|
||||
List<TurnEvidence> evidence = snapshotEvidence(session == null ? null : session.turnEvidence());
|
||||
return roundIndex < evidence.size() ? evidence.get(roundIndex) : null;
|
||||
}
|
||||
|
||||
private void rememberCustomerLine(ActiveSession session, int roundIndex, String customerText) {
|
||||
if (session == null || isBlank(customerText)) {
|
||||
return;
|
||||
|
||||
Reference in New Issue
Block a user