106 lines
5.3 KiB
Java
106 lines
5.3 KiB
Java
package ru.pdguard.api;
|
|
|
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
|
import org.slf4j.Logger;
|
|
import org.slf4j.LoggerFactory;
|
|
import org.springframework.http.HttpStatus;
|
|
import org.springframework.http.ResponseEntity;
|
|
import org.springframework.web.bind.annotation.PostMapping;
|
|
import org.springframework.web.bind.annotation.RequestBody;
|
|
import org.springframework.web.bind.annotation.RequestHeader;
|
|
import org.springframework.web.bind.annotation.RestController;
|
|
import ru.pdguard.config.SystemPolicy;
|
|
import ru.pdguard.config.SystemsConfig;
|
|
import ru.pdguard.core.LlmClient;
|
|
import ru.pdguard.core.Pipeline;
|
|
|
|
import java.util.Map;
|
|
|
|
/**
|
|
* Демонстрационное плечо к языковой модели: показывает всю цепочку целиком.
|
|
*
|
|
* <pre>
|
|
* потребитель → маскирование → LLM → демаскирование → потребитель
|
|
* </pre>
|
|
*
|
|
* <p>В ответе видны все три текста — что ушло в модель, что она вернула и что
|
|
* получил потребитель. Это и есть доказательство, что в модель не попало ничего
|
|
* незамаскированного, а ответ вернулся с восстановленными значениями.
|
|
*
|
|
* <p>Ответ модели — другой текст, а не тот же самый, поэтому восстановить его по
|
|
* идентификатору целиком нельзя: замена идёт пофрагментно. Звёздочки для этого не
|
|
* годятся — одна и та же маска отвечала бы разным значениям, — поэтому здесь всегда
|
|
* применяется обратимая подстановка, независимо от режима маскирования системы.
|
|
*
|
|
* <p>Контракт проверяющей системы это плечо не затрагивает: он живёт в
|
|
* {@link ProcessResource}.
|
|
*/
|
|
@RestController
|
|
public class ProxyResource {
|
|
|
|
private static final Logger LOG = LoggerFactory.getLogger(ProxyResource.class);
|
|
|
|
public record ProxyRequest(@JsonProperty("prompt") String prompt) {
|
|
}
|
|
|
|
public record ProxyResponse(
|
|
@JsonProperty("prompt_masked") String promptMasked,
|
|
@JsonProperty("llm_response_masked") String llmResponseMasked,
|
|
@JsonProperty("response") String response,
|
|
@JsonProperty("replaced") Map<String, String> replaced,
|
|
@JsonProperty("llm") String llm,
|
|
@JsonProperty("error") String error) {
|
|
public ProxyResponse {
|
|
replaced = replaced == null ? null : Map.copyOf(replaced);
|
|
}
|
|
}
|
|
|
|
private final Pipeline pipeline;
|
|
private final SystemsConfig systems;
|
|
private final LlmClient llm;
|
|
|
|
public ProxyResource(Pipeline pipeline, SystemsConfig systems, LlmClient llm) {
|
|
this.pipeline = pipeline;
|
|
this.systems = systems;
|
|
this.llm = llm;
|
|
}
|
|
|
|
@PostMapping("/proxy")
|
|
public ResponseEntity<ProxyResponse> proxy(@RequestBody(required = false) ProxyRequest request,
|
|
@RequestHeader(value = ProcessResource.SYSTEM_HEADER, required = false) String systemId,
|
|
@RequestHeader(value = ProcessResource.KEY_HEADER, required = false) String systemKey) {
|
|
if (request == null || request.prompt() == null || request.prompt().isBlank()) {
|
|
return ResponseEntity.badRequest()
|
|
.body(new ProxyResponse(null, null, null, null, null, "поле prompt обязательно"));
|
|
}
|
|
|
|
SystemPolicy policy = systems.policyFor(systemId);
|
|
if (!policy.accepts(systemKey)) {
|
|
return ResponseEntity.status(HttpStatus.FORBIDDEN)
|
|
.body(new ProxyResponse(null, null, null, null, null, "Неверный ключ системы"));
|
|
}
|
|
if (!policy.enabled()) {
|
|
return ResponseEntity.status(HttpStatus.FORBIDDEN)
|
|
.body(new ProxyResponse(null, null, null, null, null,
|
|
"Системе " + systemId + " обращение в модуль запрещено"));
|
|
}
|
|
|
|
Pipeline.Masked masked = pipeline.maskWithRestorations(request.prompt(), policy);
|
|
LlmClient.Answer answer = llm.ask(masked.text());
|
|
String restored = policy.demask() ? restore(answer.text(), masked.restorations()) : answer.text();
|
|
|
|
LOG.info("proxy: система={} заменено={} модель={}",
|
|
policy.name(), masked.restorations().size(), answer.source());
|
|
return ResponseEntity.ok(new ProxyResponse(masked.text(), answer.text(), restored,
|
|
masked.restorations(), answer.source(), null));
|
|
}
|
|
|
|
/** Возвращает исходные значения на место подстановок в ответе модели. */
|
|
private static String restore(String text, Map<String, String> restorations) {
|
|
String result = text;
|
|
for (Map.Entry<String, String> entry : restorations.entrySet()) {
|
|
result = result.replace(entry.getKey(), entry.getValue());
|
|
}
|
|
return result;
|
|
}
|
|
} |