# advisors Companion code for [Writing Custom Advisors in Spring AI 2.0: Logging, PII Redaction and Token Budgets](https://ankurm.com/spring-ai-2-0-custom-advisors-logging-pii-redaction-token-budgets/), part of the [Spring AI series](../README.md) on ankurm.com. Three advisors of our own -- a logger, a PII redactor and a token budget -- plus the tests that pin down how the advisor chain orders them, what happens on a stream, and what a caller sees when one of them refuses a request. There is no live model anywhere. [`RecordingModel`](src/test/java/com/ankurm/advisors/support/RecordingModel.java) records every prompt it is sent and answers with a fixed sentence, which is exactly what an advisor test needs: *what reached the model, and what came back*. No database, no Docker, no API key. ## Versions | Component | Version | |---|---| | Spring Boot | 4.1.1 | | Spring AI | 2.0.1 (`spring-ai-client-chat` 2.0.1) | | Reactor Core | 3.8.7 | | JTokkit (token estimates) | 1.1.0 | | Java | 25 (LTS) | ## Quickstart ```bash scripts/run-all.sh # runs all 21 tests and regenerates output/01 .. 21 ``` Two consecutive runs produce byte-identical files. To run the app itself: `OPENAI_API_KEY=... mvn spring-boot:run`, then `POST /chat` with an `X-User` header and `{"text": "..."}`. ## What's here | File | What it shows | |---|---| | [`advisor/Orders.java`](src/main/java/com/ankurm/advisors/advisor/Orders.java) | The order numbers, and the layers they sit between (memory +200, tool loop +300, model last) | | [`advisor/LoggingAdvisor.java`](src/main/java/com/ankurm/advisors/advisor/LoggingAdvisor.java) | Request / response / stream / failure lines; content off by default | | [`advisor/PiiRedactor.java`](src/main/java/com/ankurm/advisors/advisor/PiiRedactor.java) | Email, Luhn-checked card number and phone patterns; `` placeholders | | [`advisor/PiiRedactionAdvisor.java`](src/main/java/com/ankurm/advisors/advisor/PiiRedactionAdvisor.java) | Redact on the way in, restore on the way out, on calls and on streams (buffers a split placeholder) | | [`advisor/TokenBudgetAdvisor.java`](src/main/java/com/ankurm/advisors/advisor/TokenBudgetAdvisor.java) | Per-request and per-user limits, checked before the model is called | | [`advisor/Texts.java`](src/main/java/com/ankurm/advisors/advisor/Texts.java) | Rewriting request and response text with the `mutate()` copies | | [`config/AdvisorConfig.java`](src/main/java/com/ankurm/advisors/config/AdvisorConfig.java) | The advisors as beans and one `ChatClient` that uses them with Spring AI's memory advisor | | [`web/ChatController.java`](src/main/java/com/ankurm/advisors/web/ChatController.java) | `POST /chat`; the user comes from a header only so the tests need no login | ## Output files Every file is written by the test named in the right column, and every console block in the article is quoted from one of them. | File | Written by | |---|---| | `01-chain-order.txt`, `02-chain-order-ties.txt`, `03-chain-contents.txt` | `ChainOrderTest` | | `04-base-advisor-stream.txt` | `BaseAdvisorStreamTest` | | `05-logging-advisor.txt`, `06-logging-failure.txt` | `LoggingAdvisorTest` | | `07-pii-redaction.txt`, `08-pii-limits.txt`, `09-pii-stream-boundary.txt` | `PiiRedactionTest` | | `10-pii-order.txt` | `PiiOrderTest` | | `11-pii-multi-turn.txt` | `PiiMultiTurnTest` | | `12-token-budget.txt`, `13-token-budget-stream.txt`, `14-token-budget-stream-refusal.txt` | `TokenBudgetTest` | | `15-tool-loop-order.txt` | `ToolLoopOrderTest` | | `16-error-propagation.txt` | `ErrorPropagationTest` | | `17-context.txt` | `ContextAndImmutabilityTest` | | `18-unit-test-no-model.txt` | `UnitTestingWithoutModelTest` | | `19-web-429.txt` | `AdvisorWebTest` | | `20-built-in-orders.txt` | `BuiltInOrdersTest` | | `21-tool-loop-budget.txt` | `ToolLoopBudgetTest` | `TokenBudgetTest.aStreamIsRefusedAsAnErrorSignal` makes Spring AI's `MessageAggregator` log an `ERROR ... Aggregation Error` stack trace on the console. That is the refusal being reported, not a test failure. ## Requirements JDK 25 and Maven.