LLM-agnostic guardrails for structured content extraction. Define your fields, schema, and output format. armco handles prompt construction, input sanitization, and LLM communication. Swap models without changing your application code.
Here's an example which allows you to see the implementation structure as I expect it to be used.
- Java 17+
- Maven 3.8+
<dependency>
<groupId>io.github.jroden2</groupId>
<artifactId>armco</artifactId>
<version>1.0.7</version>
</dependency>armco is built around three concepts:
LlmProvider is a single-method interface you implement to connect any LLM. The library has no SDK dependency and no opinion on which model you use.
PromptTemplateSource defines where your prompt template comes from, either hardcoded inline in your source giving you git history on prompt changes, or loaded from a classpath file.
ExtractionRequest defines what to extract, in what format, and the expected output schema, built using a fluent builder.
Your prompt template supports four placeholders:
| Placeholder | Description |
|---|---|
{FIELDS} |
The fields to extract, rendered as a bullet list |
{FORMAT} |
The output format, e.g. json |
{SCHEMA} |
The expected output structure |
{CONTENT} |
The sanitized input content |
A default template is included at /prompts/metadata_extraction.txt on the classpath.
You can use this directly or provide your own.
armco automatically sanitizes input content before it reaches the prompt. This includes truncating input to 32,000 characters, stripping non-printable control characters, and removing the internal delimiter string if it appears in the input. Your primary injection protection comes from the structural delimiter wrapping in the prompt itself. Sanitization reinforces this rather than relying on keyword blocklists.
| Source | Method |
|---|---|
| Classpath file | PromptTemplateSource.fromClasspath(path) |
| Inline string | PromptTemplateSource.fromString(template) |
If your team prefers prompt changes to be visible in code review alongside the Java that uses them,
use fromString with a text block. If you prefer prompts to live separately from application code
and be swappable without recompiling, use fromClasspath.
// Classpath file
ArmcoClient client = new ArmcoClient(
provider,
PromptTemplateSource.fromClasspath("/prompts/metadata_extraction.txt")
);
// Inline string
ArmcoClient client = new ArmcoClient(
provider,
PromptTemplateSource.fromString("""
You are a metadata extraction engine.
...
{FIELDS}
{FORMAT}
{SCHEMA}
{CONTENT}
""")
);The example below uses the OpenAI Java SDK, but any HTTP client or LLM SDK works. armco only
requires that you implement LlmProvider, which is a single method returning a String.
<dependency>
<groupId>com.openai</groupId>
<artifactId>openai-java</artifactId>
<version>2.4.0</version>
</dependency>OpenAIClient openAiClient = OpenAIOkHttpClient.builder()
.apiKey(System.getenv("OPENAI_API_KEY"))
.build();
LlmProvider provider = prompt -> {
var response = openAiClient.chat().completions().create(
ChatCompletionCreateParams.builder()
.model(ChatModel.GPT_4_1)
.addUserMessage(prompt)
.build()
);
return response.choices().get(0).message().content().orElseThrow();
};ArmcoClient client = new ArmcoClient(
provider,
PromptTemplateSource.fromClasspath("/prompts/metadata_extraction.txt")
);String schema = """
{
"title": string | null,
"description": string | null,
"keywords": string[],
"human_context": string | null
}
""";
ExtractionRequest request = ExtractionRequest.builder(rawContent)
.fields("title", "description", "keywords", "human_context")
.outputFormat(OutputFormat.JSON)
.schema(schema)
.build();
String response = client.extract(request);armco returns the raw LLM response as a String. Parsing is left to the caller since you control the schema and output format.
ObjectMapper objectMapper = new ObjectMapper();
MetadataResult result = objectMapper.readValue(response, MetadataResult.class);@Configuration
public class ArmcoConfig {
@Bean
public LlmProvider llmProvider(@Value("${openai.api-key}") String apiKey) {
OpenAIClient client = OpenAIOkHttpClient.builder().apiKey(apiKey).build();
return prompt -> client.chat().completions().create(
ChatCompletionCreateParams.builder()
.model(ChatModel.GPT_4_1)
.addUserMessage(prompt)
.build()
).choices().get(0).message().content().orElseThrow();
}
@Bean
public ArmcoClient armcoClient(LlmProvider llmProvider) {
return new ArmcoClient(
llmProvider,
PromptTemplateSource.fromClasspath("/prompts/metadata_extraction.txt")
);
}
}MIT License — see LICENSE for details.