chore: initialize wygj monorepo
This commit is contained in:
@@ -0,0 +1,57 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<parent>
|
||||
<groupId>org.dromara</groupId>
|
||||
<artifactId>ruoyi-modules</artifactId>
|
||||
<version>${revision}</version>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<artifactId>ruoyi-aihr</artifactId>
|
||||
|
||||
<description>
|
||||
物业 AI 人力资源 Demo 业务模块
|
||||
</description>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.dromara</groupId>
|
||||
<artifactId>ruoyi-common-core</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.dromara</groupId>
|
||||
<artifactId>ruoyi-common-mybatis</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.dromara</groupId>
|
||||
<artifactId>ruoyi-common-web</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.dromara</groupId>
|
||||
<artifactId>ruoyi-system</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.apache.tika</groupId>
|
||||
<artifactId>tika-core</artifactId>
|
||||
<version>3.2.2</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.apache.tika</groupId>
|
||||
<artifactId>tika-parser-pdf-module</artifactId>
|
||||
<version>3.2.2</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.apache.tika</groupId>
|
||||
<artifactId>tika-parser-microsoft-module</artifactId>
|
||||
<version>3.2.2</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
package org.dromara.aihr.controller;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.dromara.aihr.domain.AihrCaseDto.CurateRequest;
|
||||
import org.dromara.aihr.domain.AihrCaseDto.CurateResponse;
|
||||
import org.dromara.aihr.domain.AihrCaseDto.OrganizeRequest;
|
||||
import org.dromara.aihr.domain.AihrCaseDto.OrganizeResponse;
|
||||
import org.dromara.aihr.domain.AihrCaseDto.UploadRequest;
|
||||
import org.dromara.aihr.domain.AihrCaseDto.UploadResponse;
|
||||
import org.dromara.aihr.service.AihrCaseSeedService;
|
||||
import org.dromara.common.core.domain.R;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
/**
|
||||
* 案例语音整理 seed API。
|
||||
*/
|
||||
@RequiredArgsConstructor
|
||||
@RestController
|
||||
@RequestMapping("/api/knowledge/case")
|
||||
public class AihrCaseController {
|
||||
|
||||
private final AihrCaseSeedService caseSeedService;
|
||||
|
||||
@PostMapping("/upload")
|
||||
public R<UploadResponse> upload(@RequestBody UploadRequest request) {
|
||||
return R.ok(caseSeedService.upload(request));
|
||||
}
|
||||
|
||||
@PostMapping("/organize")
|
||||
public R<OrganizeResponse> organize(@RequestBody OrganizeRequest request) {
|
||||
return R.ok(caseSeedService.organize(request));
|
||||
}
|
||||
|
||||
@PostMapping("/curate")
|
||||
public R<CurateResponse> curate(@RequestBody CurateRequest request) {
|
||||
return R.ok(caseSeedService.curate(request));
|
||||
}
|
||||
}
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
package org.dromara.aihr.controller;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.dromara.aihr.domain.AihrInterviewDto.AnswerRequest;
|
||||
import org.dromara.aihr.domain.AihrInterviewDto.AnswerResponse;
|
||||
import org.dromara.aihr.domain.AihrInterviewDto.FinishRequest;
|
||||
import org.dromara.aihr.domain.AihrInterviewDto.FinishResponse;
|
||||
import org.dromara.aihr.domain.AihrInterviewDto.StartRequest;
|
||||
import org.dromara.aihr.domain.AihrInterviewDto.StartResponse;
|
||||
import org.dromara.aihr.service.AihrInterviewSeedService;
|
||||
import org.dromara.common.core.domain.R;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
/**
|
||||
* AI 面试 seed API。
|
||||
*/
|
||||
@RequiredArgsConstructor
|
||||
@RestController
|
||||
@RequestMapping("/api/recruit/interview")
|
||||
public class AihrInterviewController {
|
||||
|
||||
private final AihrInterviewSeedService interviewSeedService;
|
||||
|
||||
@PostMapping("/start")
|
||||
public R<StartResponse> start(@RequestBody StartRequest request) {
|
||||
return R.ok(interviewSeedService.start(request));
|
||||
}
|
||||
|
||||
@PostMapping("/answer")
|
||||
public R<AnswerResponse> answer(@RequestBody AnswerRequest request) {
|
||||
return R.ok(interviewSeedService.answer(request));
|
||||
}
|
||||
|
||||
@PostMapping("/finish")
|
||||
public R<FinishResponse> finish(@RequestBody FinishRequest request) {
|
||||
return R.ok(interviewSeedService.finish(request));
|
||||
}
|
||||
}
|
||||
+85
@@ -0,0 +1,85 @@
|
||||
package org.dromara.aihr.controller;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.dromara.aihr.domain.AihrModelDto.ChatRequest;
|
||||
import org.dromara.aihr.domain.AihrModelDto.ChatResponse;
|
||||
import org.dromara.aihr.domain.AihrModelDto.ConfigEnabledRequest;
|
||||
import org.dromara.aihr.domain.AihrModelDto.ConfigRequest;
|
||||
import org.dromara.aihr.domain.AihrModelDto.ConfigResponse;
|
||||
import org.dromara.aihr.domain.AihrModelDto.ProviderRequest;
|
||||
import org.dromara.aihr.domain.AihrModelDto.ProviderResponse;
|
||||
import org.dromara.aihr.domain.AihrModelDto.ProviderStatusRequest;
|
||||
import org.dromara.aihr.service.AihrModelSeedService;
|
||||
import org.dromara.common.core.domain.R;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PatchMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.PutMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 模型能力最小迁移 API。
|
||||
*/
|
||||
@RequiredArgsConstructor
|
||||
@RestController
|
||||
@RequestMapping("/api/aihr/model")
|
||||
public class AihrModelController {
|
||||
|
||||
private final AihrModelSeedService modelSeedService;
|
||||
|
||||
@GetMapping("/providers")
|
||||
public R<List<ProviderResponse>> providers() {
|
||||
return R.ok(modelSeedService.providers());
|
||||
}
|
||||
|
||||
@PostMapping("/providers")
|
||||
public R<Void> createProvider(@RequestBody ProviderRequest request) {
|
||||
modelSeedService.createProvider(request);
|
||||
return R.ok();
|
||||
}
|
||||
|
||||
@PutMapping("/providers/{providerCode}")
|
||||
public R<Void> updateProvider(@PathVariable String providerCode, @RequestBody ProviderRequest request) {
|
||||
modelSeedService.updateProvider(providerCode, request);
|
||||
return R.ok();
|
||||
}
|
||||
|
||||
@PatchMapping("/providers/{providerCode}/status")
|
||||
public R<Void> updateProviderStatus(@PathVariable String providerCode, @RequestBody ProviderStatusRequest request) {
|
||||
modelSeedService.updateProviderStatus(providerCode, request);
|
||||
return R.ok();
|
||||
}
|
||||
|
||||
@GetMapping("/configs")
|
||||
public R<List<ConfigResponse>> configs() {
|
||||
return R.ok(modelSeedService.configs());
|
||||
}
|
||||
|
||||
@PostMapping("/configs")
|
||||
public R<Void> createConfig(@RequestBody ConfigRequest request) {
|
||||
modelSeedService.createConfig(request);
|
||||
return R.ok();
|
||||
}
|
||||
|
||||
@PutMapping("/configs/{id}")
|
||||
public R<Void> updateConfig(@PathVariable Long id, @RequestBody ConfigRequest request) {
|
||||
modelSeedService.updateConfig(id, request);
|
||||
return R.ok();
|
||||
}
|
||||
|
||||
@PatchMapping("/configs/{id}/enabled")
|
||||
public R<Void> updateConfigEnabled(@PathVariable Long id, @RequestBody ConfigEnabledRequest request) {
|
||||
modelSeedService.updateConfigEnabled(id, request);
|
||||
return R.ok();
|
||||
}
|
||||
|
||||
@PostMapping("/chat")
|
||||
public R<ChatResponse> chat(@RequestBody(required = false) ChatRequest request) {
|
||||
return R.ok(modelSeedService.chat(request));
|
||||
}
|
||||
}
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
package org.dromara.aihr.controller;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.dromara.aihr.domain.AihrPracticeDto.FinishRequest;
|
||||
import org.dromara.aihr.domain.AihrPracticeDto.FinishResponse;
|
||||
import org.dromara.aihr.domain.AihrPracticeDto.StartRequest;
|
||||
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.AihrPracticeSeedService;
|
||||
import org.dromara.common.core.domain.R;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
/**
|
||||
* 三角色对练 seed API。
|
||||
*/
|
||||
@RequiredArgsConstructor
|
||||
@RestController
|
||||
@RequestMapping("/api/train/practice")
|
||||
public class AihrPracticeController {
|
||||
|
||||
private final AihrPracticeSeedService practiceSeedService;
|
||||
|
||||
@PostMapping("/start")
|
||||
public R<StartResponse> start(@RequestBody StartRequest request) {
|
||||
return R.ok(practiceSeedService.start(request));
|
||||
}
|
||||
|
||||
@PostMapping("/turn")
|
||||
public R<TurnResponse> turn(@RequestBody TurnRequest request) {
|
||||
return R.ok(practiceSeedService.turn(request));
|
||||
}
|
||||
|
||||
@PostMapping("/finish")
|
||||
public R<FinishResponse> finish(@RequestBody FinishRequest request) {
|
||||
return R.ok(practiceSeedService.finish(request));
|
||||
}
|
||||
}
|
||||
+63
@@ -0,0 +1,63 @@
|
||||
package org.dromara.aihr.controller;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.dromara.aihr.domain.AihrSopDto.LocalImportRequest;
|
||||
import org.dromara.aihr.domain.AihrSopDto.LocalImportResponse;
|
||||
import org.dromara.aihr.domain.AihrSopDto.LocalImportTaskResponse;
|
||||
import org.dromara.aihr.domain.AihrSopDto.ProcessingOverviewResponse;
|
||||
import org.dromara.aihr.domain.AihrSopDto.SearchRequest;
|
||||
import org.dromara.aihr.domain.AihrSopDto.SearchResponse;
|
||||
import org.dromara.aihr.domain.AihrSopDto.UploadResponse;
|
||||
import org.dromara.aihr.service.AihrSopSeedService;
|
||||
import org.dromara.common.core.domain.R;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestPart;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* SOP 知识库 seed API。
|
||||
*/
|
||||
@RequiredArgsConstructor
|
||||
@RestController
|
||||
@RequestMapping("/api/knowledge")
|
||||
public class AihrSopController {
|
||||
|
||||
private final AihrSopSeedService sopSeedService;
|
||||
|
||||
@PostMapping("/search")
|
||||
public R<SearchResponse> search(@RequestBody SearchRequest request) {
|
||||
return R.ok(sopSeedService.search(request));
|
||||
}
|
||||
|
||||
@PostMapping("/doc/upload")
|
||||
public R<UploadResponse> uploadDoc(@RequestPart("file") MultipartFile file, @RequestParam("category") String category) {
|
||||
return R.ok(sopSeedService.uploadDoc(file, category));
|
||||
}
|
||||
|
||||
@PostMapping("/doc/import-local")
|
||||
public R<LocalImportResponse> importLocalDocs(@RequestBody LocalImportRequest request) {
|
||||
return R.ok(sopSeedService.importLocalDocs(request));
|
||||
}
|
||||
|
||||
@PostMapping("/doc/import-local-task")
|
||||
public R<LocalImportTaskResponse> startLocalImportTask(@RequestBody LocalImportRequest request) {
|
||||
return R.ok(sopSeedService.startLocalImportTask(request));
|
||||
}
|
||||
|
||||
@GetMapping("/doc/import-tasks")
|
||||
public R<List<LocalImportTaskResponse>> localImportTasks() {
|
||||
return R.ok(sopSeedService.localImportTasks());
|
||||
}
|
||||
|
||||
@GetMapping("/processing/overview")
|
||||
public R<ProcessingOverviewResponse> processingOverview() {
|
||||
return R.ok(sopSeedService.processingOverview());
|
||||
}
|
||||
}
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
package org.dromara.aihr.domain;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public final class AihrCaseDto {
|
||||
|
||||
private AihrCaseDto() {
|
||||
}
|
||||
|
||||
public record UploadRequest(String fileName, String projectExtOrgId) {
|
||||
}
|
||||
|
||||
public record UploadResponse(String caseId, String fileName, String transcript, List<TagResponse> tags) {
|
||||
}
|
||||
|
||||
public record OrganizeRequest(String caseId) {
|
||||
}
|
||||
|
||||
public record OrganizeResponse(String caseId, List<SummaryResponse> summary, List<TagResponse> tags, String aiSummary) {
|
||||
}
|
||||
|
||||
public record CurateRequest(String caseId, String criteria, Integer limit) {
|
||||
}
|
||||
|
||||
public record CurateResponse(List<String> selectedCaseIds, List<RecordResponse> records, String sampleHint) {
|
||||
}
|
||||
|
||||
public record SummaryResponse(String label, String text) {
|
||||
}
|
||||
|
||||
public record TagResponse(String text, String type) {
|
||||
}
|
||||
|
||||
public record RecordResponse(String title, String type, String status, String time, String owner, String summary) {
|
||||
}
|
||||
}
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
package org.dromara.aihr.domain;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public final class AihrInterviewDto {
|
||||
|
||||
private AihrInterviewDto() {
|
||||
}
|
||||
|
||||
public record StartRequest(String candidateId, String positionCode, String mode) {
|
||||
}
|
||||
|
||||
public record StartResponse(String sessionId, String candidateId, String candidateName, String positionCode, List<QuestionResponse> questions) {
|
||||
}
|
||||
|
||||
public record QuestionResponse(String questionId, Integer seq, String questionText, String point, String scenario, String sampleAnswer) {
|
||||
}
|
||||
|
||||
public record AnswerRequest(String sessionId, String questionId, String answerText, String answerAudioUrl) {
|
||||
}
|
||||
|
||||
public record AnswerResponse(Integer score, Map<String, Integer> dimensions, String aiComment) {
|
||||
}
|
||||
|
||||
public record FinishRequest(String sessionId) {
|
||||
}
|
||||
|
||||
public record FinishResponse(
|
||||
Integer totalScore,
|
||||
String suggestion,
|
||||
String result,
|
||||
String aiSummary,
|
||||
String evidence,
|
||||
List<DimensionResponse> dimensions,
|
||||
List<RecordResponse> records
|
||||
) {
|
||||
}
|
||||
|
||||
public record DimensionResponse(String label, Object value, String note) {
|
||||
}
|
||||
|
||||
public record RecordResponse(String time, String candidate, String position, Integer score, String result, String summary) {
|
||||
}
|
||||
}
|
||||
+83
@@ -0,0 +1,83 @@
|
||||
package org.dromara.aihr.domain;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public final class AihrModelDto {
|
||||
|
||||
private AihrModelDto() {
|
||||
}
|
||||
|
||||
public record ProviderResponse(
|
||||
Long id,
|
||||
String providerName,
|
||||
String providerCode,
|
||||
String providerDesc,
|
||||
String apiHost,
|
||||
String status,
|
||||
Integer sortOrder,
|
||||
String source,
|
||||
boolean configured
|
||||
) {
|
||||
}
|
||||
|
||||
public record ConfigResponse(
|
||||
Long id,
|
||||
String category,
|
||||
String modelName,
|
||||
String providerCode,
|
||||
String modelDescribe,
|
||||
Integer modelDimension,
|
||||
String modelShow,
|
||||
String apiHost,
|
||||
boolean enabled,
|
||||
boolean apiKeyConfigured,
|
||||
boolean configured
|
||||
) {
|
||||
}
|
||||
|
||||
public record ProviderRequest(
|
||||
String providerName,
|
||||
String providerCode,
|
||||
String providerDesc,
|
||||
String apiHost,
|
||||
String apiKey,
|
||||
String status,
|
||||
Integer sortOrder,
|
||||
String remark
|
||||
) {
|
||||
}
|
||||
|
||||
public record ProviderStatusRequest(String status) {
|
||||
}
|
||||
|
||||
public record ConfigRequest(
|
||||
String category,
|
||||
String modelName,
|
||||
String providerCode,
|
||||
String modelDescribe,
|
||||
Integer modelDimension,
|
||||
String modelShow,
|
||||
String apiHost,
|
||||
String apiKey,
|
||||
Boolean enabled,
|
||||
String remark
|
||||
) {
|
||||
}
|
||||
|
||||
public record ConfigEnabledRequest(Boolean enabled) {
|
||||
}
|
||||
|
||||
public record ChatRequest(String prompt, String model, String systemPrompt) {
|
||||
}
|
||||
|
||||
public record ChatResponse(
|
||||
boolean configured,
|
||||
String providerCode,
|
||||
String modelName,
|
||||
String answer,
|
||||
String mode,
|
||||
String error,
|
||||
List<String> hints
|
||||
) {
|
||||
}
|
||||
}
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
package org.dromara.aihr.domain;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public final class AihrPracticeDto {
|
||||
|
||||
private AihrPracticeDto() {
|
||||
}
|
||||
|
||||
public record StartRequest(String extPartyId, String scenarioId, String mode) {
|
||||
}
|
||||
|
||||
public record StartResponse(
|
||||
String sessionId,
|
||||
String scenarioId,
|
||||
String scenarioName,
|
||||
String trainee,
|
||||
String customerText,
|
||||
Integer emotion,
|
||||
Integer trust,
|
||||
String customerAudioUrl,
|
||||
PersonaResponse persona
|
||||
) {
|
||||
}
|
||||
|
||||
public record PersonaResponse(String customer, String project, String strategy, String goal) {
|
||||
}
|
||||
|
||||
public record TurnRequest(String sessionId, Integer roundIndex, String traineeText, String traineeAudioUrl) {
|
||||
}
|
||||
|
||||
public record TurnResponse(
|
||||
String customerText,
|
||||
String customerAudioUrl,
|
||||
Integer emotion,
|
||||
Integer trust,
|
||||
String coachHint,
|
||||
Integer roundIndex,
|
||||
Boolean finished
|
||||
) {
|
||||
}
|
||||
|
||||
public record FinishRequest(String sessionId) {
|
||||
}
|
||||
|
||||
public record FinishResponse(
|
||||
Integer totalScore,
|
||||
List<DimensionResponse> score,
|
||||
String mentorRewrite,
|
||||
String aiComment,
|
||||
List<Integer> trustCurve,
|
||||
List<RecordResponse> records
|
||||
) {
|
||||
}
|
||||
|
||||
public record DimensionResponse(String label, Object value, String note) {
|
||||
}
|
||||
|
||||
public record RecordResponse(String time, String trainee, String scene, Integer score, String status, String summary) {
|
||||
}
|
||||
}
|
||||
+114
@@ -0,0 +1,114 @@
|
||||
package org.dromara.aihr.domain;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public final class AihrSopDto {
|
||||
|
||||
private AihrSopDto() {
|
||||
}
|
||||
|
||||
public record SearchRequest(String queryText, String category, Integer limit) {
|
||||
}
|
||||
|
||||
public record UploadResponse(String docId, Long ossId, String fileName, String category, Integer fragments, String summary, List<String> tags, List<SnippetResponse> snippets) {
|
||||
}
|
||||
|
||||
public record LocalImportRequest(String directory, String category, Integer limit) {
|
||||
}
|
||||
|
||||
public record LocalImportResponse(String root, String directory, String category, Integer discovered, Integer imported, Integer failed, List<LocalImportItemResponse> items) {
|
||||
}
|
||||
|
||||
public record LocalImportItemResponse(String fileName, String status, String message, Integer fragments) {
|
||||
}
|
||||
|
||||
public record LocalImportTaskResponse(
|
||||
Long id,
|
||||
String directory,
|
||||
String category,
|
||||
String status,
|
||||
String statusType,
|
||||
Integer progress,
|
||||
Integer totalFiles,
|
||||
Integer importedFiles,
|
||||
Integer failedFiles,
|
||||
String currentFile,
|
||||
String createdAt,
|
||||
String updatedAt,
|
||||
String remark
|
||||
) {
|
||||
}
|
||||
|
||||
public record SearchResponse(
|
||||
String queryText,
|
||||
String category,
|
||||
String answer,
|
||||
String reference,
|
||||
List<DocResponse> docs,
|
||||
List<SnippetResponse> snippets,
|
||||
List<String> training,
|
||||
List<RecordResponse> records
|
||||
) {
|
||||
}
|
||||
|
||||
public record DocResponse(String title, String desc, String hit, String type) {
|
||||
}
|
||||
|
||||
public record SnippetResponse(String title, String text) {
|
||||
}
|
||||
|
||||
public record RecordResponse(String title, String category, String version, String status, String owner, String summary) {
|
||||
}
|
||||
|
||||
public record ProcessingOverviewResponse(
|
||||
List<MetricResponse> metrics,
|
||||
List<CategoryStatResponse> categories,
|
||||
List<ProcessingTaskResponse> tasks,
|
||||
List<PipelineStepResponse> pipeline,
|
||||
List<ProcessingEventResponse> events,
|
||||
ProcessingRulesResponse rules
|
||||
) {
|
||||
}
|
||||
|
||||
public record MetricResponse(String key, String label, String value, String note, String type) {
|
||||
}
|
||||
|
||||
public record CategoryStatResponse(String name, Integer files, String size, String icon, String type) {
|
||||
}
|
||||
|
||||
public record ProcessingTaskResponse(
|
||||
Long id,
|
||||
String category,
|
||||
String fileName,
|
||||
String fileType,
|
||||
String size,
|
||||
String status,
|
||||
String statusType,
|
||||
Integer progress,
|
||||
Integer fragments,
|
||||
String vectorStatus,
|
||||
String vectorType,
|
||||
String updatedAt,
|
||||
String action,
|
||||
String remark,
|
||||
String summary,
|
||||
List<String> tags,
|
||||
String classifyReason
|
||||
) {
|
||||
}
|
||||
|
||||
public record PipelineStepResponse(String name, String value, String status) {
|
||||
}
|
||||
|
||||
public record ProcessingEventResponse(String time, String taskNo, String fileName, String event, String result, String resultType, String detail) {
|
||||
}
|
||||
|
||||
public record ProcessingRulesResponse(
|
||||
Boolean parserEnabled,
|
||||
Boolean imageOcrEnabled,
|
||||
Boolean lowConfidenceRefusal,
|
||||
Boolean citationRequired,
|
||||
Integer failedTasks
|
||||
) {
|
||||
}
|
||||
}
|
||||
+72
@@ -0,0 +1,72 @@
|
||||
package org.dromara.aihr.service;
|
||||
|
||||
import org.dromara.aihr.domain.AihrCaseDto.CurateRequest;
|
||||
import org.dromara.aihr.domain.AihrCaseDto.CurateResponse;
|
||||
import org.dromara.aihr.domain.AihrCaseDto.OrganizeRequest;
|
||||
import org.dromara.aihr.domain.AihrCaseDto.OrganizeResponse;
|
||||
import org.dromara.aihr.domain.AihrCaseDto.RecordResponse;
|
||||
import org.dromara.aihr.domain.AihrCaseDto.SummaryResponse;
|
||||
import org.dromara.aihr.domain.AihrCaseDto.TagResponse;
|
||||
import org.dromara.aihr.domain.AihrCaseDto.UploadRequest;
|
||||
import org.dromara.aihr.domain.AihrCaseDto.UploadResponse;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* ponytail: JSON seed, replace with multipart + ASR when real audio is in scope.
|
||||
*/
|
||||
@Service
|
||||
public class AihrCaseSeedService {
|
||||
|
||||
private static final CaseSeed GARAGE_WATER = new CaseSeed(
|
||||
"case-garage-water",
|
||||
"车库积水投诉_20260702.wav",
|
||||
"业主反映地下车库长期积水,担心车辆受损。管家先确认车位号与积水时间,安排工程人员排查排水沟,并在业主群同步处理进度。",
|
||||
List.of(
|
||||
new TagResponse("投诉处理", "pill-success"),
|
||||
new TagResponse("车库", "pill-primary"),
|
||||
new TagResponse("高情绪", "pill-warning")
|
||||
),
|
||||
List.of(
|
||||
new SummaryResponse("背景", "雨后车库积水,业主担心车辆与安全问题。"),
|
||||
new SummaryResponse("处理", "确认位置、派工排查、群内同步、次日复盘。"),
|
||||
new SummaryResponse("结果", "排水沟堵塞已清理,业主确认问题解决。"),
|
||||
new SummaryResponse("亮点", "响应快、过程透明、主动同步进度。")
|
||||
),
|
||||
new RecordResponse(
|
||||
"车库积水投诉处理 · seed入库",
|
||||
"投诉处理",
|
||||
"已入库",
|
||||
"07-02 10:50",
|
||||
"培训组",
|
||||
"ASR 转写后自动整理为案例稿,完成送审与入库,可用于三角色对练复用。"
|
||||
)
|
||||
);
|
||||
|
||||
public UploadResponse upload(UploadRequest request) {
|
||||
String fileName = request == null || request.fileName() == null || request.fileName().isBlank()
|
||||
? GARAGE_WATER.fileName()
|
||||
: request.fileName();
|
||||
return new UploadResponse(GARAGE_WATER.id(), fileName, GARAGE_WATER.transcript(), GARAGE_WATER.tags());
|
||||
}
|
||||
|
||||
public OrganizeResponse organize(OrganizeRequest request) {
|
||||
return new OrganizeResponse(GARAGE_WATER.id(), GARAGE_WATER.summary(), GARAGE_WATER.tags(), GARAGE_WATER.record().summary());
|
||||
}
|
||||
|
||||
public CurateResponse curate(CurateRequest request) {
|
||||
int limit = request == null || request.limit() == null ? 1 : Math.max(request.limit(), 1);
|
||||
return new CurateResponse(List.of(GARAGE_WATER.id()).subList(0, Math.min(limit, 1)), List.of(GARAGE_WATER.record()), "已命中车库积水投诉处理样片,可用于演示案例沉淀后的培训复用。");
|
||||
}
|
||||
|
||||
private record CaseSeed(
|
||||
String id,
|
||||
String fileName,
|
||||
String transcript,
|
||||
List<TagResponse> tags,
|
||||
List<SummaryResponse> summary,
|
||||
RecordResponse record
|
||||
) {
|
||||
}
|
||||
}
|
||||
+139
@@ -0,0 +1,139 @@
|
||||
package org.dromara.aihr.service;
|
||||
|
||||
import org.dromara.aihr.domain.AihrInterviewDto.AnswerRequest;
|
||||
import org.dromara.aihr.domain.AihrInterviewDto.AnswerResponse;
|
||||
import org.dromara.aihr.domain.AihrInterviewDto.DimensionResponse;
|
||||
import org.dromara.aihr.domain.AihrInterviewDto.FinishRequest;
|
||||
import org.dromara.aihr.domain.AihrInterviewDto.FinishResponse;
|
||||
import org.dromara.aihr.domain.AihrInterviewDto.QuestionResponse;
|
||||
import org.dromara.aihr.domain.AihrInterviewDto.RecordResponse;
|
||||
import org.dromara.aihr.domain.AihrInterviewDto.StartRequest;
|
||||
import org.dromara.aihr.domain.AihrInterviewDto.StartResponse;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* ponytail: seed service only; replace with DB + LLM adapter after frontend API flow is stable.
|
||||
*/
|
||||
@Service
|
||||
public class AihrInterviewSeedService {
|
||||
|
||||
private final Map<String, CandidateSeed> candidates = buildCandidates();
|
||||
|
||||
public StartResponse start(StartRequest request) {
|
||||
CandidateSeed candidate = resolveCandidate(request == null ? null : request.candidateId(), null);
|
||||
String sessionId = "seed-" + candidate.id() + "-" + System.currentTimeMillis();
|
||||
return new StartResponse(sessionId, candidate.id(), candidate.name(), candidate.position(), candidate.questions());
|
||||
}
|
||||
|
||||
public AnswerResponse answer(AnswerRequest request) {
|
||||
String answerText = request == null ? "" : request.answerText();
|
||||
int baseScore = answerText == null || answerText.isBlank() ? 0 : 82;
|
||||
return new AnswerResponse(
|
||||
baseScore,
|
||||
Map.of("clarity", baseScore, "sop", Math.max(baseScore - 4, 0), "empathy", Math.min(baseScore + 5, 100)),
|
||||
baseScore == 0 ? "未收到有效回答" : "回答已暂存,评分将在 finish 时统一生成"
|
||||
);
|
||||
}
|
||||
|
||||
public FinishResponse finish(FinishRequest request) {
|
||||
CandidateSeed candidate = resolveCandidate(null, request == null ? null : request.sessionId());
|
||||
ScoreSeed score = candidate.score();
|
||||
return new FinishResponse(
|
||||
score.total(),
|
||||
score.suggestion(),
|
||||
score.result(),
|
||||
score.summary(),
|
||||
score.evidence(),
|
||||
score.dimensions(),
|
||||
List.of(new RecordResponse("07-02 10:35", candidate.name(), candidate.position(), score.total(), score.result(), score.summary()))
|
||||
);
|
||||
}
|
||||
|
||||
private CandidateSeed resolveCandidate(String candidateId, String sessionId) {
|
||||
if (candidateId != null && candidates.containsKey(candidateId)) {
|
||||
return candidates.get(candidateId);
|
||||
}
|
||||
if (sessionId != null) {
|
||||
for (Map.Entry<String, CandidateSeed> entry : candidates.entrySet()) {
|
||||
if (sessionId.contains(entry.getKey())) {
|
||||
return entry.getValue();
|
||||
}
|
||||
}
|
||||
}
|
||||
return candidates.get("zhang-ming");
|
||||
}
|
||||
|
||||
private static Map<String, CandidateSeed> buildCandidates() {
|
||||
Map<String, CandidateSeed> result = new LinkedHashMap<>();
|
||||
result.put("zhang-ming", new CandidateSeed(
|
||||
"zhang-ming",
|
||||
"张明",
|
||||
"生活顾问",
|
||||
List.of(
|
||||
new QuestionResponse("q1", 1, "业主因漏水情绪激动到前台投诉,你第一句话怎么说?", "考察情绪安抚、诉求确认、SOP 起手动作", "投诉接待",
|
||||
"我会先说“您先别着急,我先把情况完整记下来,今天一定给您明确反馈”。随后确认漏水位置、影响范围和是否已经报修,再同步工程人员到场时间。"),
|
||||
new QuestionResponse("q2", 2, "催缴物业费时,业主质疑服务不到位,你如何回应?", "考察费用争议、服务改进、冲突降级", "费用沟通",
|
||||
"我会先承认服务感受需要重视,把业主提到的问题列成清单,同时说明费用缴纳和服务整改可以并行推进。对争议事项约定复核时限,必要时请主管一起回访。"),
|
||||
new QuestionResponse("q3", 3, "突发停水时,多名业主集中追问,你如何组织信息同步?", "考察群体沟通、反馈时限和跨部门协同", "突发事件",
|
||||
"我会先确认是否已有工程排查结论,再在业主群发布统一口径,包括影响范围、预计恢复时间和下一次更新时间。现场安排同事分流解释,避免多人重复给出不同说法。")
|
||||
),
|
||||
new ScoreSeed(
|
||||
86,
|
||||
"建议:进入复试,重点补强催缴冲突话术。",
|
||||
"建议复试",
|
||||
"服务意识较好,能先安抚再确认诉求,催缴争议处理需要补强。",
|
||||
"命中 SOP:先安抚情绪、确认诉求、明确反馈时限;遗漏:费用争议的升级处理路径还不够明确。",
|
||||
List.of(
|
||||
new DimensionResponse("表达清晰", 86, "结构完整"),
|
||||
new DimensionResponse("SOP 命中", 82, "漏升级路径"),
|
||||
new DimensionResponse("情绪安抚", 90, "开场稳定"),
|
||||
new DimensionResponse("录用风险", "低", "需复试确认")
|
||||
)
|
||||
)
|
||||
));
|
||||
result.put("li-na", new CandidateSeed(
|
||||
"li-na",
|
||||
"李娜",
|
||||
"客服管家",
|
||||
List.of(
|
||||
new QuestionResponse("q1", 1, "业主反映报修两天没人跟进,你如何核实并回复?", "考察问题核实、责任确认和反馈时限", "报修跟进",
|
||||
"我会先查报修工单和工程派单记录,确认卡点在哪里,再向业主说明当前状态和下一步处理时间。如果确实超时,我会先道歉并同步主管跟进。"),
|
||||
new QuestionResponse("q2", 2, "业主在群里连续表达不满,你如何避免情绪扩散?", "考察群体沟通和单点安抚", "社群沟通",
|
||||
"我会在群里先给出统一、准确的处理进展,再私聊业主了解细节,避免群内反复争论。后续在约定时间把结果同步到群里。"),
|
||||
new QuestionResponse("q3", 3, "回访满意度低于预期时,你会如何复盘?", "考察复盘意识和改进动作", "满意度维护",
|
||||
"我会拆分低分原因,是响应慢、解释不清还是结果未达预期,再对照工单节点找责任点。能当天补救的先补救,不能当天解决的给出明确计划。")
|
||||
),
|
||||
new ScoreSeed(
|
||||
78,
|
||||
"建议:进入观察名单,需补现场协调和群体沟通训练。",
|
||||
"观察",
|
||||
"回答完整但反馈时限不够明确,建议补充 SOP 训练。",
|
||||
"命中 SOP:核实工单、私聊安抚、结果回访;遗漏:跨部门升级责任人和节点确认不够具体。",
|
||||
List.of(
|
||||
new DimensionResponse("表达清晰", 80, "逻辑完整"),
|
||||
new DimensionResponse("SOP 命中", 74, "升级不足"),
|
||||
new DimensionResponse("情绪安抚", 82, "能稳住群聊"),
|
||||
new DimensionResponse("录用风险", "中", "需带教")
|
||||
)
|
||||
)
|
||||
));
|
||||
return result;
|
||||
}
|
||||
|
||||
private record CandidateSeed(String id, String name, String position, List<QuestionResponse> questions, ScoreSeed score) {
|
||||
}
|
||||
|
||||
private record ScoreSeed(
|
||||
Integer total,
|
||||
String suggestion,
|
||||
String result,
|
||||
String summary,
|
||||
String evidence,
|
||||
List<DimensionResponse> dimensions
|
||||
) {
|
||||
}
|
||||
}
|
||||
+511
@@ -0,0 +1,511 @@
|
||||
package org.dromara.aihr.service;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.node.ArrayNode;
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.dromara.aihr.domain.AihrModelDto.ChatRequest;
|
||||
import org.dromara.aihr.domain.AihrModelDto.ChatResponse;
|
||||
import org.dromara.aihr.domain.AihrModelDto.ConfigEnabledRequest;
|
||||
import org.dromara.aihr.domain.AihrModelDto.ConfigRequest;
|
||||
import org.dromara.aihr.domain.AihrModelDto.ConfigResponse;
|
||||
import org.dromara.aihr.domain.AihrModelDto.ProviderRequest;
|
||||
import org.dromara.aihr.domain.AihrModelDto.ProviderResponse;
|
||||
import org.dromara.aihr.domain.AihrModelDto.ProviderStatusRequest;
|
||||
import org.springframework.dao.DataAccessException;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.net.URI;
|
||||
import java.net.http.HttpClient;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.time.Duration;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* Minimal model boundary adapted from ruoyi-ai chat_model/chat_provider.
|
||||
*/
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class AihrModelSeedService {
|
||||
|
||||
private static final String DEFAULT_PROVIDER = "custom_api";
|
||||
private static final String DEFAULT_MODEL = "gpt-4o-mini";
|
||||
private static final String TENANT_ID = "000000";
|
||||
|
||||
private final ObjectMapper objectMapper;
|
||||
private final JdbcTemplate jdbcTemplate;
|
||||
|
||||
public List<ProviderResponse> providers() {
|
||||
List<ProviderResponse> rows = dbProviders();
|
||||
return rows.isEmpty() ? seedProviders() : mergeProviders(rows);
|
||||
}
|
||||
|
||||
public List<ConfigResponse> configs() {
|
||||
List<ConfigResponse> rows = dbConfigs();
|
||||
return rows.isEmpty() ? seedConfigs() : mergeConfigs(rows);
|
||||
}
|
||||
|
||||
private static List<ProviderResponse> seedProviders() {
|
||||
return List.of(
|
||||
new ProviderResponse(-1L, "OpenAI 兼容接口", DEFAULT_PROVIDER, "OPENAI兼容格式", "自定义", "0", 0, "seed", false),
|
||||
new ProviderResponse(-2L, "DeepSeek", "deepseek", "DeepSeek 官方 API", "https://api.deepseek.com", "0", 1, "ruoyi-ai", false),
|
||||
new ProviderResponse(-3L, "智谱 AI", "zhipu", "智谱 AI 大模型服务", "https://open.bigmodel.cn", "0", 2, "ruoyi-ai", false),
|
||||
new ProviderResponse(-4L, "阿里云百炼", "qianwen", "阿里云百炼大模型服务", "https://dashscope.aliyuncs.com/compatible-mode/v1", "0", 3, "ruoyi-ai", false),
|
||||
new ProviderResponse(-5L, "Ollama", "ollama", "本地 Ollama 服务", "http://127.0.0.1:11434/v1", "0", 4, "ruoyi-ai", false)
|
||||
);
|
||||
}
|
||||
|
||||
private static List<ConfigResponse> seedConfigs() {
|
||||
return List.of(
|
||||
new ConfigResponse(-1L, "chat", DEFAULT_MODEL, DEFAULT_PROVIDER, "Demo 对话与 RAG 生成模型", null, "Y", null, true, false, false),
|
||||
new ConfigResponse(-2L, "vector", "embedding-3", "zhipu", "后续用于知识片段向量化", 2048, "N", null, false, false, false),
|
||||
new ConfigResponse(-3L, "rerank", "rerank", "zhipu", "后续用于召回片段重排序", null, "N", null, false, false, false)
|
||||
);
|
||||
}
|
||||
|
||||
private static List<ProviderResponse> mergeProviders(List<ProviderResponse> rows) {
|
||||
Map<String, ProviderResponse> merged = new LinkedHashMap<>();
|
||||
for (ProviderResponse seed : seedProviders()) {
|
||||
merged.put(seed.providerCode(), seed);
|
||||
}
|
||||
for (ProviderResponse row : rows) {
|
||||
merged.put(row.providerCode(), row);
|
||||
}
|
||||
return List.copyOf(merged.values());
|
||||
}
|
||||
|
||||
private static List<ConfigResponse> mergeConfigs(List<ConfigResponse> rows) {
|
||||
Map<String, ConfigResponse> merged = new LinkedHashMap<>();
|
||||
for (ConfigResponse seed : seedConfigs()) {
|
||||
merged.put(configKey(seed), seed);
|
||||
}
|
||||
for (ConfigResponse row : rows) {
|
||||
merged.put(configKey(row), row);
|
||||
}
|
||||
return List.copyOf(merged.values());
|
||||
}
|
||||
|
||||
private static String configKey(ConfigResponse config) {
|
||||
return config.category() + ":" + config.providerCode() + ":" + config.modelName();
|
||||
}
|
||||
|
||||
public void createProvider(ProviderRequest request) {
|
||||
ProviderData data = providerData(request);
|
||||
jdbcTemplate.update("""
|
||||
insert into aihr_model_provider
|
||||
(tenant_id, provider_name, provider_code, provider_desc, api_host, api_key, status, sort_order, remark, create_time, update_time)
|
||||
values (?, ?, ?, ?, ?, ?, ?, ?, ?, now(), now())
|
||||
""", TENANT_ID, data.providerName(), data.providerCode(), data.providerDesc(), data.apiHost(), emptyToNull(request.apiKey()),
|
||||
data.status(), data.sortOrder(), data.remark());
|
||||
}
|
||||
|
||||
public void updateProvider(String providerCode, ProviderRequest request) {
|
||||
ProviderData data = providerData(request);
|
||||
String code = clean(providerCode);
|
||||
int updated = jdbcTemplate.update("""
|
||||
update aihr_model_provider
|
||||
set provider_name = ?, provider_desc = ?, api_host = ?,
|
||||
api_key = case when ? is null then api_key else ? end,
|
||||
status = ?, sort_order = ?, remark = ?, update_time = now()
|
||||
where tenant_id = ? and provider_code = ?
|
||||
""", data.providerName(), data.providerDesc(), data.apiHost(), emptyToNull(request.apiKey()), emptyToNull(request.apiKey()),
|
||||
data.status(), data.sortOrder(), data.remark(), TENANT_ID, code);
|
||||
requireUpdated(updated, "供应商不存在: " + code);
|
||||
}
|
||||
|
||||
public void updateProviderStatus(String providerCode, ProviderStatusRequest request) {
|
||||
String status = validStatus(request == null ? null : request.status());
|
||||
String code = required(providerCode, "供应商编码不能为空");
|
||||
int updated = jdbcTemplate.update("""
|
||||
update aihr_model_provider
|
||||
set status = ?, update_time = now()
|
||||
where tenant_id = ? and provider_code = ?
|
||||
""", status, TENANT_ID, code);
|
||||
requireUpdated(updated, "供应商不存在: " + code);
|
||||
}
|
||||
|
||||
public void createConfig(ConfigRequest request) {
|
||||
ConfigData data = configData(request);
|
||||
jdbcTemplate.update("""
|
||||
insert into aihr_model_config
|
||||
(tenant_id, category, model_name, provider_code, model_describe, model_dimension, model_show, api_host, api_key, enabled, remark, create_time, update_time)
|
||||
values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, now(), now())
|
||||
""", TENANT_ID, data.category(), data.modelName(), data.providerCode(), data.modelDescribe(), data.modelDimension(),
|
||||
data.modelShow(), data.apiHost(), emptyToNull(request.apiKey()), data.enabled(), data.remark());
|
||||
}
|
||||
|
||||
public void updateConfig(Long id, ConfigRequest request) {
|
||||
if (id == null) {
|
||||
throw new IllegalArgumentException("模型 ID 不能为空");
|
||||
}
|
||||
ConfigData data = configData(request);
|
||||
int updated = jdbcTemplate.update("""
|
||||
update aihr_model_config
|
||||
set category = ?, model_name = ?, provider_code = ?, model_describe = ?, model_dimension = ?, model_show = ?,
|
||||
api_host = ?, api_key = case when ? is null then api_key else ? end,
|
||||
enabled = ?, remark = ?, update_time = now()
|
||||
where tenant_id = ? and id = ?
|
||||
""", data.category(), data.modelName(), data.providerCode(), data.modelDescribe(), data.modelDimension(), data.modelShow(),
|
||||
data.apiHost(), emptyToNull(request.apiKey()), emptyToNull(request.apiKey()), data.enabled(), data.remark(), TENANT_ID, id);
|
||||
requireUpdated(updated, "模型不存在: " + id);
|
||||
}
|
||||
|
||||
public void updateConfigEnabled(Long id, ConfigEnabledRequest request) {
|
||||
if (id == null) {
|
||||
throw new IllegalArgumentException("模型 ID 不能为空");
|
||||
}
|
||||
int enabled = Boolean.TRUE.equals(request == null ? null : request.enabled()) ? 1 : 0;
|
||||
int updated = jdbcTemplate.update("""
|
||||
update aihr_model_config
|
||||
set enabled = ?, update_time = now()
|
||||
where tenant_id = ? and id = ?
|
||||
""", enabled, TENANT_ID, id);
|
||||
requireUpdated(updated, "模型不存在: " + id);
|
||||
}
|
||||
|
||||
public ChatResponse chat(ChatRequest request) {
|
||||
RuntimeConfig runtime = runtimeConfig(request == null ? null : request.model());
|
||||
String prompt = request == null || isBlank(request.prompt()) ? "请给出物业管家处理漏水投诉的第一步。" : request.prompt();
|
||||
String modelName = request == null || isBlank(request.model()) ? runtime.modelName() : request.model();
|
||||
|
||||
if (!runtime.configured()) {
|
||||
return new ChatResponse(
|
||||
false,
|
||||
runtime.providerCode(),
|
||||
modelName,
|
||||
"模型运行参数未配置,当前返回 seed fallback:先安抚业主情绪,确认房号、影响范围和现场照片,再创建工单并承诺首次反馈时间。",
|
||||
"seed-fallback",
|
||||
null,
|
||||
List.of(
|
||||
"在 aihr_model_provider / aihr_model_config 配置 api_host、api_key、model_name 后可切换到真实 OpenAI-compatible 调用。",
|
||||
"接口不会返回 api_key。"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
String content = callOpenAiCompatible(runtime, modelName, prompt, request == null ? null : request.systemPrompt());
|
||||
return new ChatResponse(true, runtime.providerCode(), modelName, content, "openai-compatible", null, List.of());
|
||||
} catch (Exception e) {
|
||||
return new ChatResponse(
|
||||
true,
|
||||
runtime.providerCode(),
|
||||
modelName,
|
||||
"真实模型调用失败,已回退 seed:先确认事实、同步时限、再生成工单闭环。",
|
||||
"openai-compatible-failed",
|
||||
e.getMessage(),
|
||||
List.of("检查 aihr_model_provider.api_host/api_key 和 aihr_model_config.model_name 是否与供应商一致。")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private List<ProviderResponse> dbProviders() {
|
||||
try {
|
||||
return jdbcTemplate.query("""
|
||||
select id, provider_name, provider_code, provider_desc, api_host, api_key, status, sort_order
|
||||
from aihr_model_provider
|
||||
where tenant_id = ?
|
||||
order by sort_order asc, id asc
|
||||
""", (rs, rowNum) -> new ProviderResponse(
|
||||
rs.getLong("id"),
|
||||
rs.getString("provider_name"),
|
||||
rs.getString("provider_code"),
|
||||
rs.getString("provider_desc"),
|
||||
displayHost(rs.getString("api_host"), "自定义"),
|
||||
rs.getString("status"),
|
||||
rs.getObject("sort_order", Integer.class),
|
||||
"db",
|
||||
configured(rs.getString("api_host"), "model", rs.getString("api_key"))
|
||||
), TENANT_ID);
|
||||
} catch (DataAccessException e) {
|
||||
log.debug("aihr model provider db fallback: {}", e.getMessage());
|
||||
return List.of();
|
||||
}
|
||||
}
|
||||
|
||||
private List<ConfigResponse> dbConfigs() {
|
||||
try {
|
||||
return jdbcTemplate.query("""
|
||||
select c.id, c.category, c.model_name, c.provider_code, c.model_describe, c.model_dimension,
|
||||
c.model_show, c.api_host, c.enabled,
|
||||
coalesce(nullif(c.api_host, ''), nullif(p.api_host, '')) as resolved_api_host,
|
||||
coalesce(nullif(c.api_key, ''), nullif(p.api_key, '')) as resolved_api_key
|
||||
from aihr_model_config c
|
||||
left join aihr_model_provider p
|
||||
on p.tenant_id = c.tenant_id and p.provider_code = c.provider_code
|
||||
where c.tenant_id = ?
|
||||
order by case c.category when 'chat' then 0 when 'vector' then 1 when 'rerank' then 2 else 9 end, c.id asc
|
||||
""", (rs, rowNum) -> new ConfigResponse(
|
||||
rs.getLong("id"),
|
||||
rs.getString("category"),
|
||||
rs.getString("model_name"),
|
||||
rs.getString("provider_code"),
|
||||
rs.getString("model_describe"),
|
||||
rs.getObject("model_dimension", Integer.class),
|
||||
rs.getString("model_show"),
|
||||
rs.getString("api_host"),
|
||||
rs.getInt("enabled") == 1,
|
||||
!isBlank(rs.getString("resolved_api_key")),
|
||||
configured(rs.getString("resolved_api_host"), rs.getString("model_name"), rs.getString("resolved_api_key"))
|
||||
), TENANT_ID);
|
||||
} catch (DataAccessException e) {
|
||||
log.debug("aihr model config db fallback: {}", e.getMessage());
|
||||
return List.of();
|
||||
}
|
||||
}
|
||||
|
||||
private Optional<RuntimeConfig> dbRuntimeConfig(String requestedModel) {
|
||||
try {
|
||||
List<RuntimeConfig> rows = jdbcTemplate.query(runtimeSql(requestedModel), (rs, rowNum) -> {
|
||||
String baseUrl = rs.getString("resolved_api_host");
|
||||
String modelName = rs.getString("model_name");
|
||||
String apiKey = rs.getString("resolved_api_key");
|
||||
return new RuntimeConfig(
|
||||
rs.getString("provider_code"),
|
||||
modelName,
|
||||
baseUrl,
|
||||
apiKey,
|
||||
configured(baseUrl, modelName, apiKey),
|
||||
"db"
|
||||
);
|
||||
}, runtimeArgs(requestedModel));
|
||||
return rows.stream().findFirst();
|
||||
} catch (DataAccessException e) {
|
||||
log.debug("aihr model runtime db fallback: {}", e.getMessage());
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
|
||||
private String callOpenAiCompatible(RuntimeConfig runtime, String modelName, String prompt, String systemPrompt) throws Exception {
|
||||
ObjectNode body = objectMapper.createObjectNode();
|
||||
body.put("model", modelName);
|
||||
body.put("temperature", 0.2);
|
||||
body.put("stream", false);
|
||||
|
||||
ArrayNode messages = body.putArray("messages");
|
||||
ObjectNode system = messages.addObject();
|
||||
system.put("role", "system");
|
||||
system.put("content", isBlank(systemPrompt) ? "你是物业 AI 人力资源系统助手,回答要适合 Demo 演示,简洁、可执行。" : systemPrompt);
|
||||
|
||||
ObjectNode user = messages.addObject();
|
||||
user.put("role", "user");
|
||||
user.put("content", prompt);
|
||||
|
||||
HttpRequest.Builder builder = HttpRequest.newBuilder()
|
||||
.uri(URI.create(normalizeBaseUrl(runtime.baseUrl()) + "/chat/completions"))
|
||||
.timeout(Duration.ofSeconds(60))
|
||||
.header("Content-Type", "application/json")
|
||||
.POST(HttpRequest.BodyPublishers.ofString(objectMapper.writeValueAsString(body)));
|
||||
|
||||
if (!isBlank(runtime.apiKey())) {
|
||||
builder.header("Authorization", "Bearer " + runtime.apiKey());
|
||||
}
|
||||
|
||||
HttpResponse<String> response = HttpClient.newBuilder()
|
||||
.connectTimeout(Duration.ofSeconds(15))
|
||||
.build()
|
||||
.send(builder.build(), HttpResponse.BodyHandlers.ofString());
|
||||
|
||||
if (response.statusCode() < 200 || response.statusCode() >= 300) {
|
||||
throw new IllegalStateException("LLM HTTP " + response.statusCode() + ": " + truncate(response.body(), 240));
|
||||
}
|
||||
|
||||
JsonNode root = objectMapper.readTree(response.body());
|
||||
JsonNode choices = root.path("choices");
|
||||
if (!choices.isArray() || choices.size() == 0) {
|
||||
throw new IllegalStateException("LLM response missing choices");
|
||||
}
|
||||
|
||||
String content = choices.get(0).path("message").path("content").asText();
|
||||
if (isBlank(content)) {
|
||||
throw new IllegalStateException("LLM response missing message.content");
|
||||
}
|
||||
return content;
|
||||
}
|
||||
|
||||
private RuntimeConfig runtimeConfig(String requestedModel) {
|
||||
return dbRuntimeConfig(requestedModel)
|
||||
.orElseGet(() -> new RuntimeConfig(DEFAULT_PROVIDER, fallback(requestedModel, DEFAULT_MODEL), null, null, false, "seed"));
|
||||
}
|
||||
|
||||
private static String runtimeSql(String requestedModel) {
|
||||
String modelFilter = isBlank(requestedModel) ? "" : " and c.model_name = ?";
|
||||
return """
|
||||
select c.model_name, c.provider_code,
|
||||
coalesce(nullif(c.api_host, ''), nullif(p.api_host, '')) as resolved_api_host,
|
||||
coalesce(nullif(c.api_key, ''), nullif(p.api_key, '')) as resolved_api_key
|
||||
from aihr_model_config c
|
||||
left join aihr_model_provider p
|
||||
on p.tenant_id = c.tenant_id and p.provider_code = c.provider_code
|
||||
where c.tenant_id = ?
|
||||
and c.category = 'chat'
|
||||
and c.enabled = 1
|
||||
and (p.status is null or p.status = '0')
|
||||
""" + modelFilter + """
|
||||
order by case when c.model_show = 'Y' then 0 else 1 end, c.id asc
|
||||
limit 1
|
||||
""";
|
||||
}
|
||||
|
||||
private static Object[] runtimeArgs(String requestedModel) {
|
||||
return isBlank(requestedModel) ? new Object[]{TENANT_ID} : new Object[]{TENANT_ID, requestedModel};
|
||||
}
|
||||
|
||||
private static String normalizeBaseUrl(String baseUrl) {
|
||||
String normalized = baseUrl == null ? "" : baseUrl.trim();
|
||||
while (normalized.endsWith("/")) {
|
||||
normalized = normalized.substring(0, normalized.length() - 1);
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
private static String displayHost(String value, String fallback) {
|
||||
return isBlank(value) ? fallback : value;
|
||||
}
|
||||
|
||||
private static String fallback(String value, String fallback) {
|
||||
return isBlank(value) ? fallback : value;
|
||||
}
|
||||
|
||||
private static boolean isLocalhost(String baseUrl) {
|
||||
String value = baseUrl.toLowerCase();
|
||||
return value.contains("127.0.0.1") || value.contains("localhost");
|
||||
}
|
||||
|
||||
private static boolean configured(String baseUrl, String modelName, String apiKey) {
|
||||
return !isBlank(baseUrl) && !isBlank(modelName) && (!isBlank(apiKey) || isLocalhost(baseUrl));
|
||||
}
|
||||
|
||||
private static boolean isBlank(String value) {
|
||||
return value == null || value.isBlank();
|
||||
}
|
||||
|
||||
private static String required(String value, String message) {
|
||||
String cleaned = clean(value);
|
||||
if (isBlank(cleaned)) {
|
||||
throw new IllegalArgumentException(message);
|
||||
}
|
||||
return cleaned;
|
||||
}
|
||||
|
||||
private static String clean(String value) {
|
||||
return value == null ? null : value.trim();
|
||||
}
|
||||
|
||||
private static String emptyToNull(String value) {
|
||||
String cleaned = clean(value);
|
||||
return isBlank(cleaned) ? null : cleaned;
|
||||
}
|
||||
|
||||
private static String providerCode(String value) {
|
||||
String code = required(value, "供应商编码不能为空");
|
||||
if (!code.matches("[A-Za-z0-9_-]{2,50}")) {
|
||||
throw new IllegalArgumentException("供应商编码只能包含字母、数字、下划线和横线");
|
||||
}
|
||||
return code;
|
||||
}
|
||||
|
||||
private static String category(String value) {
|
||||
String category = required(value, "模型类型不能为空");
|
||||
if (!List.of("chat", "vector", "rerank").contains(category)) {
|
||||
throw new IllegalArgumentException("模型类型只支持 chat/vector/rerank");
|
||||
}
|
||||
return category;
|
||||
}
|
||||
|
||||
private static String validStatus(String value) {
|
||||
String status = fallback(clean(value), "0");
|
||||
if (!List.of("0", "1").contains(status)) {
|
||||
throw new IllegalArgumentException("状态只支持 0/1");
|
||||
}
|
||||
return status;
|
||||
}
|
||||
|
||||
private static String modelShow(String value) {
|
||||
String show = fallback(clean(value), "Y");
|
||||
if (!List.of("Y", "N").contains(show)) {
|
||||
throw new IllegalArgumentException("是否显示只支持 Y/N");
|
||||
}
|
||||
return show;
|
||||
}
|
||||
|
||||
private static void requireUpdated(int updated, String message) {
|
||||
if (updated == 0) {
|
||||
throw new IllegalArgumentException(message);
|
||||
}
|
||||
}
|
||||
|
||||
private static ProviderData providerData(ProviderRequest request) {
|
||||
if (request == null) {
|
||||
throw new IllegalArgumentException("供应商参数不能为空");
|
||||
}
|
||||
return new ProviderData(
|
||||
required(request.providerName(), "供应商名称不能为空"),
|
||||
providerCode(request.providerCode()),
|
||||
clean(request.providerDesc()),
|
||||
emptyToNull(request.apiHost()),
|
||||
validStatus(request.status()),
|
||||
request.sortOrder() == null ? 0 : request.sortOrder(),
|
||||
clean(request.remark())
|
||||
);
|
||||
}
|
||||
|
||||
private static ConfigData configData(ConfigRequest request) {
|
||||
if (request == null) {
|
||||
throw new IllegalArgumentException("模型参数不能为空");
|
||||
}
|
||||
return new ConfigData(
|
||||
category(request.category()),
|
||||
required(request.modelName(), "模型名称不能为空"),
|
||||
providerCode(request.providerCode()),
|
||||
clean(request.modelDescribe()),
|
||||
request.modelDimension(),
|
||||
modelShow(request.modelShow()),
|
||||
emptyToNull(request.apiHost()),
|
||||
request.enabled() == null || Boolean.TRUE.equals(request.enabled()) ? 1 : 0,
|
||||
clean(request.remark())
|
||||
);
|
||||
}
|
||||
|
||||
private static String truncate(String value, int maxLength) {
|
||||
if (value == null || value.length() <= maxLength) {
|
||||
return value;
|
||||
}
|
||||
return value.substring(0, maxLength);
|
||||
}
|
||||
|
||||
private record RuntimeConfig(String providerCode, String modelName, String baseUrl, String apiKey, boolean configured, String source) {
|
||||
}
|
||||
|
||||
private record ProviderData(
|
||||
String providerName,
|
||||
String providerCode,
|
||||
String providerDesc,
|
||||
String apiHost,
|
||||
String status,
|
||||
Integer sortOrder,
|
||||
String remark
|
||||
) {
|
||||
}
|
||||
|
||||
private record ConfigData(
|
||||
String category,
|
||||
String modelName,
|
||||
String providerCode,
|
||||
String modelDescribe,
|
||||
Integer modelDimension,
|
||||
String modelShow,
|
||||
String apiHost,
|
||||
int enabled,
|
||||
String remark
|
||||
) {
|
||||
}
|
||||
}
|
||||
+188
@@ -0,0 +1,188 @@
|
||||
package org.dromara.aihr.service;
|
||||
|
||||
import org.dromara.aihr.domain.AihrPracticeDto.DimensionResponse;
|
||||
import org.dromara.aihr.domain.AihrPracticeDto.FinishRequest;
|
||||
import org.dromara.aihr.domain.AihrPracticeDto.FinishResponse;
|
||||
import org.dromara.aihr.domain.AihrPracticeDto.PersonaResponse;
|
||||
import org.dromara.aihr.domain.AihrPracticeDto.RecordResponse;
|
||||
import org.dromara.aihr.domain.AihrPracticeDto.StartRequest;
|
||||
import org.dromara.aihr.domain.AihrPracticeDto.StartResponse;
|
||||
import org.dromara.aihr.domain.AihrPracticeDto.TurnRequest;
|
||||
import org.dromara.aihr.domain.AihrPracticeDto.TurnResponse;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 三角色对练 seed 编排器。P0 先稳定前后端契约,后续再替换为会话表、ASR 和 LLM 评分。
|
||||
*/
|
||||
@Service
|
||||
public class AihrPracticeSeedService {
|
||||
|
||||
private final Map<String, ScenarioSeed> scenarios = buildScenarios();
|
||||
|
||||
public StartResponse start(StartRequest request) {
|
||||
ScenarioSeed scenario = resolveScenario(request == null ? null : request.scenarioId(), null);
|
||||
RoundSeed firstRound = scenario.rounds().get(0);
|
||||
String sessionId = "seed-" + scenario.id() + "-" + System.currentTimeMillis();
|
||||
return new StartResponse(
|
||||
sessionId,
|
||||
scenario.id(),
|
||||
scenario.name(),
|
||||
scenario.trainee(),
|
||||
firstRound.customer(),
|
||||
firstRound.emotion(),
|
||||
scenario.trustStart(),
|
||||
"",
|
||||
new PersonaResponse(scenario.customer(), scenario.project(), scenario.strategy(), scenario.goal())
|
||||
);
|
||||
}
|
||||
|
||||
public TurnResponse turn(TurnRequest request) {
|
||||
ScenarioSeed scenario = resolveScenario(null, request == null ? null : request.sessionId());
|
||||
int roundIndex = normalizeRoundIndex(request == null ? null : request.roundIndex(), scenario.rounds().size());
|
||||
RoundSeed currentRound = scenario.rounds().get(roundIndex);
|
||||
int nextRoundIndex = roundIndex + 1;
|
||||
boolean finished = nextRoundIndex >= scenario.rounds().size();
|
||||
RoundSeed nextRound = finished ? currentRound : scenario.rounds().get(nextRoundIndex);
|
||||
int trust = finished ? scenario.trustEnd() : nextRound.trust();
|
||||
return new TurnResponse(
|
||||
finished ? "" : nextRound.customer(),
|
||||
"",
|
||||
nextRound.emotion(),
|
||||
trust,
|
||||
currentRound.coachHint(),
|
||||
nextRoundIndex,
|
||||
finished
|
||||
);
|
||||
}
|
||||
|
||||
public FinishResponse finish(FinishRequest request) {
|
||||
ScenarioSeed scenario = resolveScenario(null, request == null ? null : request.sessionId());
|
||||
return new FinishResponse(
|
||||
scenario.total(),
|
||||
scenario.scores(),
|
||||
scenario.rewrite(),
|
||||
scenario.summary(),
|
||||
List.of(scenario.trustStart(), scenario.rounds().get(0).trust(), scenario.trustEnd()),
|
||||
List.of(new RecordResponse("07-02 10:42", scenario.trainee(), scenario.name(), scenario.total(), "已完成", scenario.summary()))
|
||||
);
|
||||
}
|
||||
|
||||
private ScenarioSeed resolveScenario(String scenarioId, String sessionId) {
|
||||
if (scenarioId != null && scenarios.containsKey(scenarioId)) {
|
||||
return scenarios.get(scenarioId);
|
||||
}
|
||||
if (sessionId != null) {
|
||||
for (Map.Entry<String, ScenarioSeed> entry : scenarios.entrySet()) {
|
||||
if (sessionId.contains(entry.getKey())) {
|
||||
return entry.getValue();
|
||||
}
|
||||
}
|
||||
}
|
||||
return scenarios.get("complaint-water");
|
||||
}
|
||||
|
||||
private int normalizeRoundIndex(Integer roundIndex, int size) {
|
||||
if (roundIndex == null || roundIndex < 0) {
|
||||
return 0;
|
||||
}
|
||||
return Math.min(roundIndex, size - 1);
|
||||
}
|
||||
|
||||
private static Map<String, ScenarioSeed> buildScenarios() {
|
||||
Map<String, ScenarioSeed> result = new LinkedHashMap<>();
|
||||
result.put("complaint-water", new ScenarioSeed(
|
||||
"complaint-water",
|
||||
"客服应对投诉",
|
||||
"张三",
|
||||
"高情绪、追问进度、质疑服务",
|
||||
"住宅 / 车库积水",
|
||||
"仅在漏关键步骤时提示",
|
||||
"训练学员先安抚、再确认诉求,并给出责任人和首次反馈时间。",
|
||||
68,
|
||||
82,
|
||||
List.of(
|
||||
new RoundSeed(
|
||||
"你们说今天处理,到现在都没人联系我,这个车库还怎么停?",
|
||||
"这轮命中“安抚 + 责任人 + 首次反馈时间”,可以继续追问处理承诺。",
|
||||
76,
|
||||
75
|
||||
),
|
||||
new RoundSeed(
|
||||
"我不要流程,我要知道如果今天还处理不好怎么办。",
|
||||
"补上了升级路径和兜底承诺,结束后可进入评分。",
|
||||
64,
|
||||
82
|
||||
)
|
||||
),
|
||||
List.of(
|
||||
new DimensionResponse("合规", 84, "有升级路径"),
|
||||
new DimensionResponse("沟通", 88, "承诺清楚"),
|
||||
new DimensionResponse("情绪", 82, "先承接诉求"),
|
||||
new DimensionResponse("错题", "1条", "补费用争议")
|
||||
),
|
||||
86,
|
||||
"建议表达为:“我先确认责任人和到场时间,10 分钟内回复您;如果今天无法完成,我同步主管给您替代方案和下一次反馈时间。”",
|
||||
"能先安抚并明确责任人与反馈时限,升级兜底表达比上一轮更完整。"
|
||||
));
|
||||
result.put("fee-collection", new ScenarioSeed(
|
||||
"fee-collection",
|
||||
"催缴沟通技巧",
|
||||
"李四",
|
||||
"拒绝缴费、质疑服务、要求先整改",
|
||||
"住宅 / 物业费催缴",
|
||||
"提示服务改进与缴费并行",
|
||||
"训练学员把费用沟通和服务整改拆开处理,避免激化争议。",
|
||||
61,
|
||||
76,
|
||||
List.of(
|
||||
new RoundSeed(
|
||||
"服务做成这样还让我缴费?你先把问题解决了再说。",
|
||||
"表达方向正确,但要补“争议项复核”和“责任人”。",
|
||||
72,
|
||||
68
|
||||
),
|
||||
new RoundSeed(
|
||||
"那我怎么知道你不是随便记一下?",
|
||||
"补上主管复核和节点同步,可以进入评分。",
|
||||
66,
|
||||
76
|
||||
)
|
||||
),
|
||||
List.of(
|
||||
new DimensionResponse("合规", 80, "复核明确"),
|
||||
new DimensionResponse("沟通", 83, "拆分得当"),
|
||||
new DimensionResponse("情绪", 78, "安抚一般"),
|
||||
new DimensionResponse("错题", "2条", "补开场安抚")
|
||||
),
|
||||
81,
|
||||
"建议先承接情绪:“您觉得服务不到位,我先把问题负责跟到底。”再说明缴费和整改并行,最后给出主管复核与反馈时间。",
|
||||
"费用争议拆分较清楚,但开场情绪承接不足。"
|
||||
));
|
||||
return result;
|
||||
}
|
||||
|
||||
private record ScenarioSeed(
|
||||
String id,
|
||||
String name,
|
||||
String trainee,
|
||||
String customer,
|
||||
String project,
|
||||
String strategy,
|
||||
String goal,
|
||||
Integer trustStart,
|
||||
Integer trustEnd,
|
||||
List<RoundSeed> rounds,
|
||||
List<DimensionResponse> scores,
|
||||
Integer total,
|
||||
String rewrite,
|
||||
String summary
|
||||
) {
|
||||
}
|
||||
|
||||
private record RoundSeed(String customer, String coachHint, Integer emotion, Integer trust) {
|
||||
}
|
||||
}
|
||||
+2023
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user