Tramai Logo
Tramai

Spring Boot Integration

What it is: tramai-spring provides Spring Boot auto-configuration that creates a Tramai engine instance, registers @AiService interfaces as Spring beans, and binds provider configuration from application.yml.

When to use it: You have a Spring Boot application and want TramAI services injected as standard Spring beans with YAML-driven configuration and zero manual wiring.

Minimum Setup

Step 1: Add the dependency

dependencies {
   implementation("dev.tramai:tramai-spring:0.3.1")
   implementation("dev.tramai:tramai-openai:0.3.1") // or your provider
}

Step 2: Add @EnableTramai

Put @EnableTramai on any configuration class or your main application class:

import dev.tramai.spring.EnableTramai

@SpringBootApplication
@EnableTramai
class InvoiceApplication

fun main(args: Array<String>) {
   runApplication<InvoiceApplication>(*args)
}

Step 3: Configure in YAML

tramai:
  default-provider: openai
  models:
    gpt-4o: openai
  providers:
    openai:
      api-key: ${OPENAI_API_KEY}

Step 4: Inject and Use

@Service
class BillingService(
    private val invoiceAnalyzer: InvoiceAnalyzer,  // injected Spring bean
) {
    fun process(invoiceText: String) {
        val result = invoiceAnalyzer.analyze(invoiceText)
        // ...
    }
}

That's 5 lines of YAML + 1 annotation. TramAI creates the engine, registers @AiService interfaces as Spring beans, and routes calls to gpt-4o on OpenAI.

What Auto-Configuration Provides

  • Engine creation: Binds tramai.* properties from application.yml and creates a Tramai engine instance
  • Service bean registration: Scans for @AiService interfaces and registers proxy beans (no manual Tramai.create() needed)
  • Tool discovery: Scans for @AiTool methods on @Component beans and registers them as tools
  • Secret resolution: Resolves env:NAME and file:/path/secret references natively; supports Vault and AWS Secrets Manager
  • Resilience: Configures circuit breaker, retry policy, and token budgets from YAML
  • Cache: Optional in-memory response cache

InvoiceAnalyzer Example

import dev.tramai.core.annotations.AiService
import dev.tramai.core.annotations.Operation

// 1. Define your typed result
data class InvoiceVerdict(
   val status: String,          // "approved" | "needs_review" | "rejected"
   val reason: String,
   val missingFields: List<String>,
)

// 2. Define your AI service interface
@AiService
interface InvoiceAnalyzer {
   @Operation(
       prompt = "Analyze the invoice. Return status, reason, and any missing required fields.",
       model = "gpt-4o",
   )
   suspend fun analyze(invoiceText: String): InvoiceVerdict
}

// 3. Inject and use it anywhere
@Service
class BillingService(
   private val analyzer: InvoiceAnalyzer,  // Auto-wired by Spring!
) {
   suspend fun process(invoiceText: String): InvoiceVerdict =
       analyzer.analyze(invoiceText)
}

// 4. Use it in a controller
@RestController
class InvoiceController(
   private val billingService: BillingService,
) {
   @PostMapping("/invoices/analyze")
   suspend fun analyze(@RequestBody text: String): InvoiceVerdict =
       billingService.process(text)
}

TramAI handles the LLM call, JSON extraction, parsing, validation, and retries automatically. Your code never touches HTTP clients or JSON parsers.

Multiple Providers

tramai:
  default-provider: openai
  models:
    gpt-4o: openai
    claude-sonnet-4-20250514: anthropic
    llama3.2: ollama
  providers:
    openai:
      api-key: ${OPENAI_API_KEY}
    anthropic:
      api-key: ${ANTHROPIC_API_KEY}
    ollama:
      base-url: http://localhost:11434

Operations reference models by name in @Operation(model = "claude-sonnet-4-20250514").

YAML Reference

PropertyDescriptionRequired
tramai.default-providerFallback provider nameNo (but recommended)
tramai.models.<model>Model-to-provider mappingYes (at least one)
tramai.providers.openai.api-keyOpenAI API keyConditional
tramai.providers.anthropic.api-keyAnthropic API keyConditional
tramai.providers.ollama.base-urlOllama base URLConditional
tramai.providers.openai-compatible.base-urlCustom gateway URLConditional
tramai.providers.openai-compatible.provider-nameCustom provider nameNo (default: openai-compatible)
tramai.fallbacks.<model>Fallback route configNo
tramai.resilience.circuit-breaker.enabledEnable circuit breakerNo (default: false)
tramai.resilience.circuit-breaker.failure-thresholdFailures before openNo (default: 3)
tramai.resilience.circuit-breaker.open-duration-millisOpen durationNo (default: 30000)
tramai.resilience.retry.max-retry-after-millisMax retry delayNo
tramai.resilience.retry.jitter-ratioRetry jitterNo (default: 0.1)
tramai.cost.token-budget.hard-max-tokens-per-attemptPer-attempt limitNo
tramai.cost.token-budget.hard-max-tokens-per-operationPer-operation limitNo

Fallback Routing in Spring

tramai:
  models:
    gpt-4o: openai
    gpt-4o-mini: openai
  fallbacks:
    gpt-4o:
      - provider: openai
        model: gpt-4o-mini
  providers:
    openai:
      api-key: ${OPENAI_API_KEY}

If a call to gpt-4o fails, TramAI retries with gpt-4o-mini automatically. Fallback order is explicit — the engine does not guess.

Resilience Controls

tramai:
  resilience:
    circuit-breaker:
      enabled: true
      failure-threshold: 3
      open-duration-millis: 30000
    retry:
      max-retry-after-millis: 20000
      jitter-ratio: 0.1
  cost:
    token-budget:
      hard-max-tokens-per-attempt: 4000
      hard-max-tokens-per-operation: 12000

Circuit breaker opens after 3 failures and stays open for 30 seconds. Retries are jittered. Token budgets are enforced per-attempt and per-operation.

Secret References

TramAI resolves these reference schemes natively in YAML configuration:

providers:
  openai:
    api-key-secret-ref: env:OPENAI_API_KEY

Supported schemes:

  • env:NAME — reads from environment variable
  • file:/absolute/path/to/secret.txt — reads from file

You can also provide a custom SecretValueResolver bean for Vault, AWS Secrets Manager, or other backends.

Kotlin + Java Examples

@SpringBootApplication
@EnableTramai
class SupportApp

data class TicketResponse(
   val resolution: String,
   val priority: String,
   val needsEscalation: Boolean,
)

@AiService
interface SupportAgent {
   @Operation(prompt = "Resolve the support ticket", model = "gpt-4o")
   suspend fun resolve(ticket: String): TicketResponse
}

@Service
class SupportService(
   private val agent: SupportAgent,
) {
   suspend fun handleTicket(ticket: String): TicketResponse = agent.resolve(ticket)
}

Java

import dev.tramai.spring.EnableTramai;

@SpringBootApplication
@EnableTramai
public class SupportApplication {
   public static void main(String[] args) {
       SpringApplication.run(SupportApplication.class, args);
   }
}

public record TicketResponse(
   String resolution,
   String priority,
   boolean needsEscalation
) {}

@AiService
public interface SupportAgent {
   @Operation(prompt = "Resolve the support ticket", model = "gpt-4o")
   TicketResponse resolve(String ticket);
}

@Service
public class SupportService {
   private final SupportAgent agent;

   public SupportService(SupportAgent agent) {
       this.agent = agent;
   }

   public TicketResponse handleTicket(String ticket) {
       return agent.resolve(ticket);
   }
}

Limitations

  • No advanced bean scopes: AI service beans are standard singleton proxies.
  • No per-bean provider overrides outside annotations: Provider selection is through @Operation(provider = ...) or the model mapping registry.
  • No automatic test slices: Spring Boot test slices (e.g., @WebMvcTest) don't automatically isolate TramAI. Use tramai-testing with mock providers in tests.
  • Thin adapter: The Spring module focuses on wiring — it does not add Spring-specific abstractions on top of the engine.

Next Steps