Interactive Spring Shell-based command-line interface for the Embabel Agent platform, providing terminal interaction, chat sessions, and agent management commands.
Working code examples demonstrating common use cases for Embabel Agent Shell.
Basic Spring Boot application with shell:
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication
@SpringBootApplication
class MinimalAgentApp
fun main(args: Array<String>) {
runApplication<MinimalAgentApp>(*args)
}application.yaml:
embabel:
agent:
shell:
lineLength: 140Usage:
./gradlew bootRun
embabel> agents
embabel> x "Find news about AI"Service that executes tasks programmatically:
import com.embabel.agent.api.common.autonomy.Autonomy
import com.embabel.agent.api.common.autonomy.AgentProcessExecution
import com.embabel.agent.api.common.autonomy.ProcessOptions
import com.embabel.agent.api.common.autonomy.GoalSelectionOptions
import com.embabel.agent.shell.TerminalServices
import com.embabel.agent.core.AgentPlatform
import org.springframework.stereotype.Service
@Service
class TaskExecutionService(
private val autonomy: Autonomy,
private val terminalServices: TerminalServices,
private val agentPlatform: AgentPlatform
) {
fun executeTask(intent: String, debug: Boolean = false): AgentProcessExecution {
val processOptions = ProcessOptions(
open = false,
showPrompts = debug,
showLlmResponses = debug,
debug = debug,
toolDelay = false,
operationDelay = false,
showPlanning = true,
blackboard = null
)
val goalSeeker = autonomy.createGoalSeeker(
intent = intent,
agentScope = agentPlatform,
goalChoiceApprover = terminalServices,
goalSelectionOptions = GoalSelectionOptions()
)
val outputChannel = terminalServices.outputChannel(agentPlatform)
return goalSeeker.achieve(
processOptions = processOptions,
outputChannel = outputChannel
)
}
}Usage:
@RestController
class TaskController(
private val taskExecutionService: TaskExecutionService
) {
@PostMapping("/execute")
fun execute(@RequestBody request: TaskRequest): TaskResponse {
val result = taskExecutionService.executeTask(
intent = request.intent,
debug = request.debug
)
return TaskResponse(
output = result.output.toString(),
cost = result.costInfo.totalCost
)
}
}Complete chat application with log management:
import com.embabel.agent.shell.TerminalServices
import com.embabel.chat.Chatbot
import com.embabel.chat.ChatSession
import com.embabel.agent.shell.personality.starwars.StarWarsColorPalette
import org.springframework.boot.CommandLineRunner
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication
import org.springframework.context.annotation.Bean
import org.springframework.stereotype.Component
@SpringBootApplication
class ChatApplication
fun main(args: Array<String>) {
runApplication<ChatApplication>(*args)
}
@Component
class ChatRunner(
private val terminalServices: TerminalServices,
private val chatbot: Chatbot
) : CommandLineRunner {
override fun run(vararg args: String?) {
// Redirect logs during chat
val restoreLogging = terminalServices.redirectLoggingToFile(
filename = "chat-session",
dir = System.getProperty("user.dir")
)
try {
val chatSession = chatbot.createSession()
val welcome = """
Welcome to Star Wars Agent Chat!
May the Force be with you.
Type 'exit' to end the session.
""".trimIndent()
terminalServices.chat(
chatSession = chatSession,
welcome = welcome,
colorPalette = StarWarsColorPalette()
)
} finally {
// Restore original logging
restoreLogging()
}
}
}application.yaml:
embabel:
agent:
shell:
lineLength: 120
redirectLogToFile: true
logging:
personality: starwarsCreate and use a custom prompt provider:
import com.embabel.agent.shell.MessageGeneratorPromptProvider
import com.embabel.common.util.MessageGenerator
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty
import org.springframework.stereotype.Component
// Custom message generator
class RobotMessageGenerator : MessageGenerator {
private val messages = listOf(
"System: All systems operational",
"System: Standing by for commands",
"System: Ready to assist",
"System: Processing request",
"System: Analyzing data"
)
override fun generate(): String {
return messages.random()
}
}
// Custom prompt provider
@Component
@ConditionalOnProperty(
name = ["embabel.agent.logging.personality"],
havingValue = "robot"
)
class RobotPromptProvider : MessageGeneratorPromptProvider(
prompt = "robot",
color = 0x00FFFF, // Cyan
messageGenerator = RobotMessageGenerator()
)application.yaml:
embabel:
agent:
logging:
personality: robotResult: Cyan "robot>" prompt with random system messages
Format process output with custom styling:
import com.embabel.agent.shell.formatProcessOutput
import com.embabel.agent.shell.markdownToConsole
import com.embabel.agent.api.common.autonomy.AgentProcessExecution
import com.embabel.agent.spi.logging.ColorPalette
import com.embabel.agent.spi.logging.DefaultColorPalette
import com.fasterxml.jackson.databind.ObjectMapper
import org.springframework.stereotype.Service
@Service
class OutputFormattingService(
private val objectMapper: ObjectMapper,
private val colorPalette: ColorPalette = DefaultColorPalette()
) {
fun formatExecutionResult(result: AgentProcessExecution): String {
return formatProcessOutput(
result = result,
colorPalette = colorPalette,
objectMapper = objectMapper,
lineLength = 140
)
}
fun formatMarkdown(content: String): String {
return if (content.contains("#")) {
markdownToConsole(content)
} else {
content
}
}
fun formatWithCustomLineLength(
result: AgentProcessExecution,
lineLength: Int
): String {
return formatProcessOutput(
result = result,
colorPalette = colorPalette,
objectMapper = objectMapper,
lineLength = lineLength
)
}
}Usage:
@Component
class ResultPresenter(
private val outputFormattingService: OutputFormattingService
) {
fun presentResult(result: AgentProcessExecution) {
val formatted = outputFormattingService.formatExecutionResult(result)
println(formatted)
}
fun presentMarkdown(markdown: String) {
val styled = outputFormattingService.formatMarkdown(markdown)
println(styled)
}
}Handle interactive forms with validation:
import com.embabel.agent.shell.TerminalServices
import com.embabel.agent.api.common.autonomy.Awaitable
import com.embabel.agent.api.common.autonomy.FormBindingRequest
import com.embabel.agent.api.common.autonomy.ConfirmationRequest
import com.embabel.agent.core.AgentProcess
import org.springframework.stereotype.Service
@Service
class FormHandlerService(
private val terminalServices: TerminalServices
) {
fun handleInteractiveRequest(
awaitable: Awaitable<*, *>,
agentProcess: AgentProcess
): Boolean {
val response = terminalServices.handleAwaitable(awaitable)
return if (response != null) {
awaitable.onResponse(response, agentProcess)
true
} else {
// User cancelled
false
}
}
fun confirmAction(message: String): Boolean {
return terminalServices.confirm(message)
}
fun confirmWithContext(
action: String,
context: Map<String, Any>
): Boolean {
val contextStr = context.entries.joinToString("\n") {
" ${it.key}: ${it.value}"
}
val message = """
Confirm action: $action
Context:
$contextStr
Proceed?
""".trimIndent()
return terminalServices.confirm(message)
}
}Usage:
@Component
class WorkflowExecutor(
private val formHandlerService: FormHandlerService
) {
fun executeWithConfirmation() {
val context = mapOf(
"task" to "Delete user data",
"user" to "john@example.com",
"records" to 150
)
if (formHandlerService.confirmWithContext("Delete Data", context)) {
// Proceed with action
println("Action confirmed, proceeding...")
} else {
println("Action cancelled")
}
}
}Add custom commands to the shell:
import org.springframework.shell.standard.ShellComponent
import org.springframework.shell.standard.ShellMethod
import org.springframework.shell.standard.ShellOption
import com.embabel.agent.shell.TerminalServices
import com.embabel.agent.api.common.autonomy.Autonomy
@ShellComponent
class CustomCommands(
private val autonomy: Autonomy,
private val terminalServices: TerminalServices
) {
@ShellMethod(
value = "Quick search for news",
key = ["news", "n"]
)
fun news(
@ShellOption(help = "Search topic") topic: String
): String {
val intent = "Find recent news about $topic"
// Execute task and return result
return "Searching for news about: $topic"
}
@ShellMethod(
value = "Summarize text",
key = ["summarize", "sum"]
)
fun summarize(
@ShellOption(help = "Text to summarize") text: String,
@ShellOption(
value = ["-l", "--length"],
help = "Summary length",
defaultValue = "short"
) length: String
): String {
val intent = "Summarize the following text in $length form: $text"
// Execute task and return result
return "Generating $length summary..."
}
@ShellMethod(value = "Interactive task builder")
fun buildTask(): String {
terminalServices.print("Task Builder")
terminalServices.print("============")
if (terminalServices.confirm("Do you want to search the web?")) {
terminalServices.print("Web search enabled")
}
return "Task configuration complete"
}
}Usage:
embabel> news "artificial intelligence"
embabel> n "AI"
embabel> summarize "Long text here..." -l brief
embabel> sum "Text..." --length detailed
embabel> buildTaskHandle multi-turn conversations with state:
import com.embabel.agent.shell.TerminalServices
import com.embabel.chat.Chatbot
import com.embabel.chat.ChatSession
import org.springframework.stereotype.Service
@Service
class ConversationService(
private val terminalServices: TerminalServices,
private val chatbot: Chatbot
) {
fun runConversation(topic: String) {
val chatSession = chatbot.createSession()
val welcome = """
Starting conversation about: $topic
I'll remember our conversation context.
Type 'exit' to end.
""".trimIndent()
// Redirect logs for clean output
val restoreLogging = terminalServices.redirectLoggingToFile(
filename = "conversation-$topic",
dir = System.getProperty("user.dir")
)
try {
terminalServices.chat(
chatSession = chatSession,
welcome = welcome
)
} finally {
restoreLogging()
}
println("\nConversation ended. Logs saved to: conversation-$topic.log")
}
fun runMultiTopicConversation(topics: List<String>) {
topics.forEach { topic ->
println("\n=== Topic: $topic ===\n")
runConversation(topic)
}
}
}Usage:
@Component
class ConversationRunner(
private val conversationService: ConversationService
) : CommandLineRunner {
override fun run(vararg args: String?) {
val topics = listOf(
"technology",
"science",
"business"
)
conversationService.runMultiTopicConversation(topics)
}
}Implement custom goal approval logic:
import com.embabel.agent.api.common.autonomy.GoalChoiceApprover
import com.embabel.agent.api.common.autonomy.GoalChoiceApprovalRequest
import com.embabel.agent.api.common.autonomy.GoalChoiceApprovalResponse
import com.embabel.agent.api.common.autonomy.GoalChoiceApproved
import com.embabel.agent.api.common.autonomy.GoalChoiceNotApproved
import com.embabel.agent.shell.TerminalServices
import org.springframework.stereotype.Component
@Component
class CustomGoalApprover(
private val terminalServices: TerminalServices
) : GoalChoiceApprover {
private val autoApprovedGoals = setOf(
"search_news",
"get_weather",
"calculate"
)
override fun approve(
goalChoiceApprovalRequest: GoalChoiceApprovalRequest
): GoalChoiceApprovalResponse {
val goal = goalChoiceApprovalRequest.goalChoice.goal
// Auto-approve safe goals
if (goal.name in autoApprovedGoals) {
terminalServices.print("Auto-approved goal: ${goal.name}")
return GoalChoiceApproved(goalChoiceApprovalRequest)
}
// Require explicit approval for other goals
val approved = terminalServices.confirm(
"""
Approve goal: ${goal.description}
Goal name: ${goal.name}
Capabilities: ${goal.capabilities.joinToString()}
Approve?
""".trimIndent()
)
return if (approved) {
GoalChoiceApproved(goalChoiceApprovalRequest)
} else {
GoalChoiceNotApproved(
goalChoiceApprovalRequest,
reason = "User declined approval"
)
}
}
}Usage:
@Service
class SafeTaskExecutor(
private val autonomy: Autonomy,
private val customGoalApprover: CustomGoalApprover,
private val agentPlatform: AgentPlatform
) {
fun executeSafely(intent: String): AgentProcessExecution {
val goalSeeker = autonomy.createGoalSeeker(
intent = intent,
agentScope = agentPlatform,
goalChoiceApprover = customGoalApprover, // Use custom approver
goalSelectionOptions = GoalSelectionOptions()
)
return goalSeeker.achieve(
processOptions = ProcessOptions(),
outputChannel = terminalServices.outputChannel(agentPlatform)
)
}
}Create a custom output channel with logging:
import com.embabel.agent.api.channel.OutputChannel
import com.embabel.agent.api.channel.OutputChannelEvent
import com.embabel.agent.api.channel.MessageOutputChannelEvent
import com.embabel.agent.api.channel.ProgressOutputChannelEvent
import com.embabel.agent.api.channel.LoggingOutputChannelEvent
import com.embabel.agent.api.channel.ContentOutputChannelEvent
import org.slf4j.LoggerFactory
import org.springframework.stereotype.Component
@Component
class LoggingOutputChannel : OutputChannel {
private val logger = LoggerFactory.getLogger(javaClass)
override fun send(event: OutputChannelEvent) {
when (event) {
is MessageOutputChannelEvent -> {
logger.info("[Message] ${event.sender}: ${event.content}")
println("${event.sender}: ${event.content}")
}
is ProgressOutputChannelEvent -> {
logger.info("[Progress] ${event.message}")
println("▶ ${event.message}")
}
is LoggingOutputChannelEvent -> {
logger.debug("[Log] ${event.message}")
println("🪵 ${event.message}")
}
is ContentOutputChannelEvent -> {
logger.info("[Content] ${event.content}")
println(event.content)
}
}
}
}
@Component
class MultiChannelOutputService(
private val terminalServices: TerminalServices,
private val loggingOutputChannel: LoggingOutputChannel,
private val agentPlatform: AgentPlatform
) {
fun createMultiChannel(): OutputChannel {
val terminalChannel = terminalServices.outputChannel(agentPlatform)
return object : OutputChannel {
override fun send(event: OutputChannelEvent) {
// Send to both channels
terminalChannel.send(event)
loggingOutputChannel.send(event)
}
}
}
}Full application with all features:
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import com.embabel.chat.Chatbot
import com.embabel.chat.ChatSession
@SpringBootApplication
class CompleteAgentApplication
fun main(args: Array<String>) {
runApplication<CompleteAgentApplication>(*args)
}
@Configuration
class AppConfiguration {
@Bean
fun chatbot(): Chatbot {
return object : Chatbot {
override fun createSession(): ChatSession {
// Return your chat session implementation
return MyChatSession()
}
}
}
}application.yaml:
spring:
application:
name: complete-agent-app
embabel:
agent:
shell:
lineLength: 140
redirectLogToFile: true
logging:
personality: starwars
logging:
level:
com.embabel.agent: INFO
root: WARNbuild.gradle.kts:
plugins {
kotlin("jvm") version "1.9.20"
kotlin("plugin.spring") version "1.9.20"
id("org.springframework.boot") version "3.2.0"
}
dependencies {
implementation("com.embabel.agent:embabel-agent-shell:0.3.3")
implementation("org.springframework.boot:spring-boot-starter")
implementation("org.springframework.shell:spring-shell-starter:3.2.0")
}Run:
./gradlew bootRun
embabel> agents
embabel> x "Find news" -p
embabel> chat
embabel> exitUnit test for shell components:
import com.embabel.agent.shell.TerminalServices
import com.embabel.agent.shell.config.ShellProperties
import org.jline.terminal.Terminal
import org.junit.jupiter.api.Test
import org.mockito.Mockito.mock
import kotlin.test.assertEquals
class TerminalServicesTest {
@Test
fun testConfirmYes() {
val terminal = mock(Terminal::class.java)
val shellProperties = ShellProperties().apply {
lineLength = 140
}
val terminalServices = TerminalServices(terminal, shellProperties)
// Mock user input "y"
// (Requires additional mocking setup)
// Test confirmation logic
// val result = terminalServices.confirm("Test?")
// assertEquals(true, result)
}
@Test
fun testLineLength() {
val shellProperties = ShellProperties().apply {
lineLength = 100
}
assertEquals(100, shellProperties.lineLength)
}
}These examples demonstrate common patterns and use cases for the Embabel Agent Shell. Adapt them to your specific needs and requirements.
tessl i tessl/maven-com-embabel-agent--embabel-agent-shell@0.3.0