Replies: 3 comments
-
|
This is something I've been experimenting with with Copilot. You can define an agent through a conversation. You describe:
You then get a planned agent and can tweak it (relatively blindly) until it is ready to "roll out". For an Embabel equivalent I like the idea of thinking of this conversation as an OODA loop where you are trying to describe and create and define a new agent using natural language harnessing a defined set of bounded context schemas (to balance the determinism). Taking the Tripper agent, you could define it with the following conversation: Agent Engineer (AE): Can you help me create a new agent? Agent Creator Agent (ACA): Sure! What's the role the agent is taking on? AE: "A Travel Planner" ACA: Great! Can you describe the sorts of things the Travel Planner agent will be able to do for you? AE: I'd like it to be able to extract a travel brief from a proposed journey for a set of travellers, finding points of interest along that journey, gathering some research about those points of interest. Checking back with the travellers on the proposed plan and revisit if necessary, then if the proposal is accepted then propose a number of sleeping options along the proposed travel plan. Once the sleeping options are added then fire back the plan to the travellers. ACA: Coupling your agent to a domain model can help the agent to work as effectively as possible (especially when evaluating the responses of LLMs!). I have a couple of bounded context domain models defined for your organisation that I could use to help make this as determinate as possible:
ACA: Would you like to see more detail on any of these contexts or are you happy for me to try and use one or more of these bounded contexts to increase the effectiveness of your agent? (If you don't see a bounded context that's suitable you can not use one or maybe ping the agent platform team to explore a new bounded context for your organisation). AE: The Travel Agent Domain look perfect! Can you use that? ACA: Using the Travel Agent Domain. Given the broad nature of the queries I also have a number of models, services and tools that I think will support your agent's work: Services:
Models (We can tune these as you explore using your new agent to get the balance of accuracy and cost):
Tools:
I also have other tools and services available inside your organisation and I can show you those if they are of interest? If you're happy using the proposed models, services and tools then I can add these to your agent? AE: Happy with those! ACA: All added. Shall I perform a test and you can see the output? AE: Sure! ACA: AE: I think we need to select some higher quality models perhaps? ACA: Here are the models I am aware of and the tasks the agent performs. Let me know what agents to use for what tasks and we can re-run the test? AE: I'd like X model for taks Y etc... ACA: Ok, re-running the test, how does it look? AE: That looks grand! Can you create a web endpoint for me to kick off this agent? ACA: Absolutely! The agent is also usable directly from here by asking me what agents are around, but a specific endpoint with a web form to kick off your new Travel Planner agent is http://xxxx AE: Great, thanks! |
Beta Was this translation helpful? Give feedback.
-
|
Another concept could be to define the whole thing in a natural language exchange: AE: I'd like to create an agent that plans travel for some travellers between two locations. I'd like it to be able to extract a travel brief from a proposed journey for a set of travellers, finding points of interest along that journey, gathering some research about those points of interest. Checking back with the travellers on the proposed plan and revisit if necessary, then if the proposal is accepted then propose a number of sleeping options along the proposed travel plan. Once the sleeping options are added then fire back the plan to the travellers. I'd like to, where possible, tie all interactions to our company's Travel Agent Domain and I'm happy for you to pick the services, tools and models that might work best. ACA: All done, your Travel planner agent is ready. Shall I perform a test and you can see the output? AE: Sure! ACA: AE: I think we need to select some higher quality models perhaps? |
Beta Was this translation helpful? Give feedback.
-
|
Using Kiro in specification and planning mode (not Vibe coding live mode) I pretty much played out the conversation described above and a design spec for the agent was generated along with a set of steps for implementation. This does look strong but the proof is going to be in the pudding a little... Details
Design DocumentOverviewThe TravelPlanner agent will be designed as a specialized travel planning agent that extends the existing agent framework in the engineering-agents application. Based on the analysis of the current codebase, the agent will follow the established patterns used by The TravelPlanner agent will be implemented in Kotlin and will leverage the Embabel Agent API annotations ( ArchitectureCore Components
Integration Points
Components and InterfacesTravelPlannerAgent@Agent(description = "A specialized travel planning agent that creates personalized itineraries and travel recommendations")
@Profile("!test")
class TravelPlannerAgent(
private val config: TravelPlannerProperties,
@Value("\${travel.planner.maxDays:14}") private val maxTripDays: Int,
@Value("\${travel.planner.maxDestinations:5}") private val maxDestinations: Int
)Configuration Properties@ConfigurationProperties("travel.planner")
data class TravelPlannerProperties(
private val plannerModelRole: String = OpenAiModels.GPT_41_MINI,
private val plannerModelTemperature: Double = 0.8,
private val reviewerModelRole: String = OpenAiModels.GPT_41_MINI,
private val reviewerModelTemperature: Double = 0.3,
val braveSearchApiKey: String = "",
val braveSearchEnabled: Boolean = true,
val maxSearchResults: Int = 10
) {
val plannerLlm = LlmOptions(criteria = byRole(plannerModelRole))
.withTemperature(plannerModelTemperature)
val reviewerLlm = LlmOptions(criteria = byRole(reviewerModelRole))
.withTemperature(reviewerModelTemperature)
}Data ModelsThe agent will define travel-specific data classes that implement the required interfaces: data class TravelBrief(
val travelers: List<Traveler>,
val journeyRoute: List<String>, // destinations in order
val duration: Int, // days
val budget: String? = null,
val interests: List<String> = emptyList(),
val travelStyle: String? = null,
val specialRequirements: List<String> = emptyList()
)
data class Traveler(
val name: String,
val age: Int? = null,
val preferences: List<String> = emptyList(),
val restrictions: List<String> = emptyList()
)
data class PointOfInterest(
val name: String,
val location: String,
val type: String, // museum, restaurant, landmark, etc.
val description: String,
val estimatedVisitTime: String,
val cost: String? = null,
val openingHours: String? = null
)
data class ResearchedPOI(
val poi: PointOfInterest,
val detailedInfo: String,
val reviews: String,
val tips: List<String>,
val nearbyAttractions: List<String>
)
data class ProposedPlan(
val brief: TravelBrief,
val pointsOfInterest: List<ResearchedPOI>,
val dailyItinerary: List<DayPlan>,
val estimatedCosts: Map<String, String>
) : HasContent, Timestamped {
override val timestamp: Instant get() = Instant.now()
override val content: String get() = """
# Proposed Travel Plan
**Travelers:** ${brief.travelers.joinToString(", ") { it.name }}
**Route:** ${brief.journeyRoute.joinToString(" → ")}
**Duration:** ${brief.duration} days
## Points of Interest
${pointsOfInterest.joinToString("\n\n") { "### ${it.poi.name}\n${it.poi.description}\n**Tips:** ${it.tips.joinToString(", ")}" }}
## Daily Itinerary
${dailyItinerary.joinToString("\n\n") { it.content }}
## Estimated Costs
${estimatedCosts.entries.joinToString("\n") { "**${it.key}:** ${it.value}" }}
**Created:** ${timestamp.atZone(ZoneId.systemDefault()).format(DateTimeFormatter.ofPattern("EEEE, MMMM dd, yyyy HH:mm:ss"))}
""".trimIndent()
}
data class TravelerFeedback(
val approved: Boolean,
val comments: String,
val requestedChanges: List<String> = emptyList()
)
data class AccommodationOption(
val name: String,
val type: String, // hotel, hostel, airbnb, etc.
val location: String,
val pricePerNight: String,
val amenities: List<String>,
val rating: Double? = null,
val description: String
)
data class FinalTravelPlan(
val proposedPlan: ProposedPlan,
val accommodations: List<AccommodationOption>,
val totalEstimatedCost: String,
val finalRecommendations: List<String>
) : HasContent, Timestamped {
override val timestamp: Instant get() = Instant.now()
override val content: String get() = """
# Final Travel Plan
${proposedPlan.content}
## Accommodation Options
${accommodations.joinToString("\n\n") {
"### ${it.name} (${it.type})\n**Location:** ${it.location}\n**Price:** ${it.pricePerNight}/night\n**Amenities:** ${it.amenities.joinToString(", ")}\n${it.description}"
}}
## Total Estimated Cost
$totalEstimatedCost
## Final Recommendations
${finalRecommendations.joinToString("\n") { "• $it" }}
**Finalized:** ${timestamp.atZone(ZoneId.systemDefault()).format(DateTimeFormatter.ofPattern("EEEE, MMMM dd, yyyy HH:mm:ss"))}
""".trimIndent()
}
data class DayPlan(
val day: Int,
val location: String,
val activities: List<String>,
val meals: List<String>,
val accommodation: String? = null,
val transportationNotes: String? = null
) {
val content: String get() = """
### Day $day - $location
**Activities:** ${activities.joinToString(", ")}
**Meals:** ${meals.joinToString(", ")}
${accommodation?.let { "**Accommodation:** $it" } ?: ""}
${transportationNotes?.let { "**Transportation:** $it" } ?: ""}
""".trimIndent()
}
data class WebSearchResult(
val title: String,
val url: String,
val snippet: String,
val publishedDate: String? = null
)
data class BraveSearchResponse(
val query: String,
val results: List<WebSearchResult>,
val searchTimestamp: Instant = Instant.now()
)Persona DefinitionsFollowing the pattern established in val JourneyAnalyst = Persona(
name = "Journey Analyst",
persona = "A detail-oriented travel analyst who excels at extracting key information from travel requests",
voice = "Methodical and thorough",
objective = "Accurately identify traveler needs, preferences, and journey requirements from user input"
)
val POIResearcher = Persona(
name = "Points of Interest Researcher",
persona = "A knowledgeable travel researcher with deep expertise in global destinations and attractions",
voice = "Informative and enthusiastic",
objective = "Discover and research compelling points of interest that match traveler preferences and journey routes"
)
val TravelPlanner = Persona(
name = "Expert Travel Planner",
persona = "An experienced travel consultant who creates well-structured itineraries and manages traveler expectations",
voice = "Professional and accommodating",
objective = "Create balanced, practical travel plans that incorporate research findings and accommodate traveler feedback"
)
val AccommodationSpecialist = Persona(
name = "Accommodation Specialist",
persona = "A hospitality expert with extensive knowledge of lodging options worldwide",
voice = "Helpful and detail-oriented",
objective = "Identify suitable accommodation options that match budget, location, and traveler preferences"
)Data ModelsInput/Output Flow
State ManagementThe agent will be stateless, following the same pattern as Error HandlingException Management
Error Response Structuredata class TravelPlanningError(
val errorType: String,
val message: String,
val details: Map<String, Any> = emptyMap()
) : HasContent {
override val content: String get() = "Travel Planning Error: $errorType - $message"
}Testing StrategyUnit TestingFollowing the pattern established in
Test Structureinternal class TravelPlannerAgentTest {
@Test
fun testExtractTravelBrief() {
val testConfig = TravelPlannerProperties()
val agent = TravelPlannerAgent(testConfig, 14, 5)
val llmCall = captureLlmCall(Runnable {
agent.extractTravelBrief(UserInput("My family of 4 wants to travel from London to Edinburgh via York for 7 days", Instant.now()))
})
// Verify prompt content and configuration for journey analysis
Assertions.assertTrue(llmCall.prompt.contains("London"))
Assertions.assertTrue(llmCall.prompt.contains("Edinburgh"))
Assertions.assertTrue(llmCall.prompt.contains("extract"))
}
@Test
fun testFindPointsOfInterest() {
val testConfig = TravelPlannerProperties()
val agent = TravelPlannerAgent(testConfig, 14, 5)
val travelBrief = TravelBrief(
travelers = listOf(Traveler("John"), Traveler("Jane")),
journeyRoute = listOf("London", "York", "Edinburgh"),
duration = 7,
interests = listOf("history", "museums")
)
val llmCall = captureLlmCall(Runnable {
agent.findPointsOfInterest(UserInput("Find attractions", Instant.now()), travelBrief)
})
// Verify POI research functionality
Assertions.assertTrue(llmCall.prompt.contains("York"))
Assertions.assertTrue(llmCall.prompt.contains("history"))
}
@Test
fun testResearchPointsOfInterest() {
val testConfig = TravelPlannerProperties()
val agent = TravelPlannerAgent(testConfig, 14, 5)
val context = FakeOperationContext.create()
context.expectResponse("Detailed research about York Minster including opening hours and visitor tips")
val poi = PointOfInterest("York Minster", "York", "cathedral", "Historic cathedral", "2 hours")
agent.researchPointsOfInterest(UserInput("Research attractions", Instant.now()), listOf(poi), context)
val llmInvocation = context.llmInvocations.single()
Assertions.assertTrue(llmInvocation.prompt.contains("York Minster"))
Assertions.assertTrue(llmInvocation.prompt.contains("research"))
}
@Test
fun testCreateProposedPlan() {
val testConfig = TravelPlannerProperties()
val agent = TravelPlannerAgent(testConfig, 14, 5)
val travelBrief = TravelBrief(
travelers = listOf(Traveler("John")),
journeyRoute = listOf("London", "Edinburgh"),
duration = 5
)
val researchedPOIs = listOf(
ResearchedPOI(
PointOfInterest("Tower of London", "London", "castle", "Historic fortress", "3 hours"),
"Detailed info", "Great reviews", listOf("Book ahead"), listOf("Tower Bridge")
)
)
val llmCall = captureLlmCall(Runnable {
agent.createProposedPlan(UserInput("Create plan", Instant.now()), travelBrief, researchedPOIs)
})
// Verify plan creation with higher temperature for creativity
Assertions.assertTrue(llmCall.prompt.contains("Tower of London"))
Assertions.assertEquals(0.8, llmCall.llm!!.temperature, 0.01)
}
@Test
fun testRevisePlanBasedOnFeedback() {
val testConfig = TravelPlannerProperties()
val agent = TravelPlannerAgent(testConfig, 14, 5)
val context = FakeOperationContext.create()
context.expectResponse("Revised plan incorporating feedback about more outdoor activities")
val originalPlan = ProposedPlan(
TravelBrief(listOf(Traveler("John")), listOf("London"), 3),
emptyList(), emptyList(), emptyMap()
)
val feedback = TravelerFeedback(false, "Need more outdoor activities", listOf("Add parks", "Include walking tours"))
agent.revisePlan(UserInput("Revise plan", Instant.now()), originalPlan, feedback, context)
val llmInvocation = context.llmInvocations.single()
Assertions.assertTrue(llmInvocation.prompt.contains("outdoor activities"))
Assertions.assertTrue(llmInvocation.prompt.contains("revise"))
}
@Test
fun testProposeAccommodations() {
val testConfig = TravelPlannerProperties()
val agent = TravelPlannerAgent(testConfig, 14, 5)
val approvedPlan = ProposedPlan(
TravelBrief(listOf(Traveler("John")), listOf("London", "Edinburgh"), 5),
emptyList(), emptyList(), mapOf("accommodation" to "£100/night")
)
val llmCall = captureLlmCall(Runnable {
agent.proposeAccommodations(UserInput("Find hotels", Instant.now()), approvedPlan)
})
// Verify accommodation search functionality
Assertions.assertTrue(llmCall.prompt.contains("London"))
Assertions.assertTrue(llmCall.prompt.contains("Edinburgh"))
Assertions.assertTrue(llmCall.prompt.contains("accommodation"))
}
@Test
fun testCreateFinalPlan() {
val testConfig = TravelPlannerProperties()
val agent = TravelPlannerAgent(testConfig, 14, 5)
val context = FakeOperationContext.create()
context.expectResponse("Final comprehensive travel plan with all details")
val approvedPlan = ProposedPlan(
TravelBrief(listOf(Traveler("John")), listOf("London"), 3),
emptyList(), emptyList(), emptyMap()
)
val accommodations = listOf(
AccommodationOption("Premier Inn", "hotel", "London", "£80/night", listOf("WiFi", "Breakfast"), 4.2, "Modern hotel")
)
agent.createFinalPlan(UserInput("Finalize plan", Instant.now()), approvedPlan, accommodations, context)
val llmInvocation = context.llmInvocations.single()
Assertions.assertTrue(llmInvocation.prompt.contains("Premier Inn"))
Assertions.assertTrue(llmInvocation.prompt.contains("final"))
}
}Integration Testing
Brave Web Search IntegrationWeb Search ServiceThe TravelPlanner agent will integrate with Brave Search API to gather real-time information about:
Web Search Implementation@Service
class BraveSearchService(
private val config: TravelPlannerProperties,
private val restTemplate: RestTemplate
) {
fun searchPointsOfInterest(location: String, interests: List<String>): BraveSearchResponse {
val query = buildSearchQuery(location, interests, "attractions points of interest")
return performSearch(query)
}
fun searchAccommodations(location: String, budget: String?, travelDates: String?): BraveSearchResponse {
val query = buildAccommodationQuery(location, budget, travelDates)
return performSearch(query)
}
fun researchSpecificPOI(poiName: String, location: String): BraveSearchResponse {
val query = "$poiName $location opening hours reviews tips"
return performSearch(query)
}
private fun performSearch(query: String): BraveSearchResponse {
if (!config.braveSearchEnabled) {
return BraveSearchResponse(query, emptyList())
}
val headers = HttpHeaders().apply {
set("X-Subscription-Token", config.braveSearchApiKey)
set("Accept", "application/json")
}
val url = "https://api.search.brave.com/res/v1/web/search?q=${URLEncoder.encode(query, "UTF-8")}&count=${config.maxSearchResults}"
return try {
val response = restTemplate.exchange(url, HttpMethod.GET, HttpEntity<String>(headers), String::class.java)
parseSearchResponse(query, response.body ?: "")
} catch (e: Exception) {
logger.warn("Brave Search API call failed for query: $query", e)
BraveSearchResponse(query, emptyList())
}
}
}Enhanced Data Models with Web Searchdata class ResearchedPOI(
val poi: PointOfInterest,
val detailedInfo: String,
val reviews: String,
val tips: List<String>,
val nearbyAttractions: List<String>,
val webSearchResults: List<WebSearchResult> = emptyList(), // Real-time web data
val lastUpdated: Instant = Instant.now()
)
data class AccommodationOption(
val name: String,
val type: String,
val location: String,
val pricePerNight: String,
val amenities: List<String>,
val rating: Double? = null,
val description: String,
val webSearchResults: List<WebSearchResult> = emptyList(), // Real-time availability/pricing
val bookingUrls: List<String> = emptyList()
)Web Search Integration in Workflow
Implementation ConsiderationsPerformance
Security
Maintainability
|
Beta Was this translation helpful? Give feedback.
Uh oh!
There was an error while loading. Please reload this page.
-
We are creating the PaaS for natural language.
Beta Was this translation helpful? Give feedback.
All reactions