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; /** * Демонстрационное плечо к языковой модели: показывает всю цепочку целиком. * *
 * потребитель → маскирование → LLM → демаскирование → потребитель
 * 
* *

В ответе видны все три текста — что ушло в модель, что она вернула и что * получил потребитель. Это и есть доказательство, что в модель не попало ничего * незамаскированного, а ответ вернулся с восстановленными значениями. * *

Ответ модели — другой текст, а не тот же самый, поэтому восстановить его по * идентификатору целиком нельзя: замена идёт пофрагментно. Звёздочки для этого не * годятся — одна и та же маска отвечала бы разным значениям, — поэтому здесь всегда * применяется обратимая подстановка, независимо от режима маскирования системы. * *

Контракт проверяющей системы это плечо не затрагивает: он живёт в * {@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 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 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 restorations) { String result = text; for (Map.Entry entry : restorations.entrySet()) { result = result.replace(entry.getKey(), entry.getValue()); } return result; } }