Files
pd-guard/src/main/java/ru/pdguard/api/ProxyResource.java
T

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;
}
}