Skip to content

Commit 97b1bbc

Browse files
committed
Upgrade generic
1 parent 9158c67 commit 97b1bbc

File tree

6 files changed

+80
-84
lines changed

6 files changed

+80
-84
lines changed
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package com.embabel.agent.web.htmx
2+
3+
import com.embabel.agent.core.AgentProcess
4+
import org.springframework.ui.Model
5+
6+
/**
7+
* Generic processing values to be used in the model for HTMX responses
8+
* across different apps.
9+
* This allows for consistent handling of agent processes and page details.
10+
*/
11+
data class GenericProcessingValues(
12+
val agentProcess: AgentProcess,
13+
val pageTitle: String,
14+
val detail: String,
15+
val resultModelKey: String,
16+
val successView: String,
17+
) {
18+
19+
fun addToModel(model: Model) {
20+
model.addAttribute("processId", agentProcess.id)
21+
model.addAttribute("pageTitle", pageTitle)
22+
model.addAttribute("detail", detail)
23+
model.addAttribute("resultModelKey", resultModelKey)
24+
model.addAttribute("successView", successView)
25+
}
26+
}

src/main/kotlin/com/embabel/movie/agent/MovieFinderAgent.kt

Lines changed: 27 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ package com.embabel.movie.agent
1717

1818
import com.embabel.agent.api.annotation.*
1919
import com.embabel.agent.api.common.OperationContext
20+
import com.embabel.agent.api.common.create
2021
import com.embabel.agent.api.common.createObject
2122
import com.embabel.agent.config.models.OpenAiModels
2223
import com.embabel.agent.core.CoreToolGroups
@@ -32,6 +33,7 @@ import com.embabel.movie.service.MovieResponse
3233
import com.embabel.movie.service.OmdbClient
3334
import com.embabel.movie.service.StreamingAvailabilityClient
3435
import com.embabel.movie.service.StreamingOption
36+
import com.fasterxml.jackson.annotation.JsonPropertyDescription
3537
import org.slf4j.LoggerFactory
3638
import org.springframework.boot.context.properties.ConfigurationProperties
3739
import org.springframework.context.annotation.Profile
@@ -76,6 +78,7 @@ data class StreamableMovie(
7678
* Structured recommendations
7779
*/
7880
data class MovieRecommendations(
81+
val caption: String,
7982
val writeup: String,
8083
val movies: List<StreamableMovie>,
8184
) : HasContent {
@@ -455,38 +458,44 @@ class MovieFinderAgent(
455458
val writeup = context.promptRunner(
456459
llm = config.llm,
457460
promptContributors = listOf(config.suggesterPersona)
458-
) generateText
459-
"""
461+
).create<MovieWriteup>(
462+
"""
460463
Write up a recommendation of ${config.suggestionCount} movies in ${config.writeupWordCount} words
461464
for ${dmb.movieBuff.name}
462465
based on the following information:
463466
Their hobbies are ${dmb.movieBuff.hobbies.joinToString(", ")}
464467
Their movie taste profile is ${dmb.tasteProfile}
465468
A bit about them: "${dmb.movieBuff.about}"
469+
470+
Include a CONCISE caption for the writeup. It should not include the movie buff's name.
466471
467472
The streamable movie recommendations are:
468473
${
469-
recommendedMovies.joinToString("\n\n") {
470-
"""
474+
recommendedMovies.joinToString("\n\n") {
475+
"""
471476
${it.movie.Title} (${it.movie.Year}): ${it.movie.imdbID}
472477
Director: ${it.movie.Director}
473478
Actors: ${it.movie.Actors}
474479
${it.movie.Plot}
475480
FREE Streaming available to ${dmb.movieBuff.name} on ${
476-
it.availableStreamingOptions.joinToString(
477-
", "
478-
) { "${it.service.name} at ${it.link}" }
479-
}
481+
it.availableStreamingOptions.joinToString(
482+
", "
483+
) { "${it.service.name} at ${it.link}" }
484+
}
480485
All streaming options: ${it.allStreamingOptions.joinToString(", ") { "${it.service.name} at ${it.link}" }}
481486
""".trimIndent()
482-
}
483487
}
488+
}
484489
485-
Format in Markdown and include links to the movies on IMDB and the streaming service link(s) for each.
490+
Format in HTML.
491+
Use unordered lists as appropriate.
492+
Start any headings at <h4>
493+
Include links to the movies on IMDB and the streaming service link(s) for each.
486494
List those with FREE streaming first and call that out.
487-
""".trimIndent()
495+
""".trimIndent())
488496
return MovieRecommendations(
489-
writeup = writeup,
497+
caption = writeup.caption,
498+
writeup = writeup.writeup,
490499
movies = recommendedMovies,
491500
)
492501
}
@@ -495,3 +504,9 @@ class MovieFinderAgent(
495504
const val HAVE_ENOUGH_MOVIES = "haveEnoughMovies"
496505
}
497506
}
507+
508+
private data class MovieWriteup(
509+
@field:JsonPropertyDescription("Caption for the writeup, to be used as a title")
510+
val caption: String,
511+
val writeup: String,
512+
)

src/main/kotlin/com/embabel/movie/web/MovieHtmxController.kt

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ package com.embabel.movie.web
1818
import com.embabel.agent.core.AgentPlatform
1919
import com.embabel.agent.core.ProcessOptions
2020
import com.embabel.agent.core.Verbosity
21+
import com.embabel.agent.web.htmx.GenericProcessingValues
2122
import com.embabel.movie.agent.MovieRequest
2223
import com.embabel.movie.domain.MovieBuffRepository
2324
import com.embabel.movie.domain.rod
@@ -50,13 +51,13 @@ class MovieHtmxController(
5051
return "find-movies-form"
5152
}
5253

53-
@PostMapping("/plan")
54+
@PostMapping("/find-movies")
5455
fun findMovies(
5556
@ModelAttribute movieRequest: MovieRequest,
5657
model: Model
5758
): String {
5859

59-
// Convert form travelers to domain objects
60+
// Convert form to domain objects
6061
val agent = agentPlatform.agents().singleOrNull { it.name.lowercase().contains("movie") }
6162
?: error("No movie agent found. Please ensure the movie agent is registered.")
6263

@@ -73,7 +74,13 @@ class MovieHtmxController(
7374
)
7475

7576
model.addAttribute("movieRequest", movieRequest)
76-
model.addAttribute("processId", agentProcess.id)
77+
GenericProcessingValues(
78+
agentProcess = agentProcess,
79+
pageTitle = "Finding Movies",
80+
detail = movieRequest.preference,
81+
resultModelKey = "movieRecommendations",
82+
successView = "movie-recommendations",
83+
).addToModel(model)
7784
agentPlatform.start(agentProcess)
7885
return "common/processing"
7986
}

src/main/resources/templates/common/processing.html

Lines changed: 5 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,7 @@ <h2 th:text="${pageTitle}"></h2>
1414
<div id="journey-container">
1515
<div id="journey-status">
1616
<div class="working">
17-
<h3>Planning your journey...</h3>
18-
<p th:text="${travelBrief.brief}"></p>
17+
<p th:text="${detail}"></p>
1918

2019
<div class="process-controls">
2120
<button type="button"
@@ -80,20 +79,7 @@ <h3>Plan</h3>
8079
switch (data.type) {
8180
case 'AgentProcessPlanFormulatedEvent':
8281
headerText = `📋 Plan Formulated`;
83-
if (data.plan && data.plan.actions) {
84-
// Choose appropriate icons based on action name
85-
function getActionIcon(actionName) {
86-
const actionLower = actionName.toLowerCase();
87-
if (actionLower.includes('car') || actionLower.includes('travel')) return '🚗';
88-
if (actionLower.includes('bus')) return '🚌';
89-
if (actionLower.includes('process')) return '⚙️';
90-
if (actionLower.includes('food') || actionLower.includes('eat') || actionLower.includes('restaurant')) return '🍽️';
91-
if (actionLower.includes('tour') || actionLower.includes('visit') || actionLower.includes('see')) return '🏛️';
92-
if (actionLower.includes('book') || actionLower.includes('reserve')) return '📝';
93-
return '☐'; // Default icon
94-
}
95-
96-
// Create a more visually appealing representation of actions
82+
if (data.plan?.actions) {
9783
const history =
9884
data.history.map(ai => ai.actionName)
9985
const planned = data.plan.actions.map(action => action.name);
@@ -219,7 +205,9 @@ <h3>Plan</h3>
219205
// Handle completion events
220206
if (data.type === "GoalAchievedEvent") {
221207
eventSource.close();
222-
window.location.href = `/status/${processId}?resultModelKey=travelPlan&successView=journey-plan`;
208+
209+
// Get values from template context
210+
window.location.href = `/status/${processId}?resultModelKey=[[${resultModelKey}]]&successView=[[${successView}]]`;
223211
return;
224212
}
225213

src/main/resources/templates/find-movies-form.html

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,15 @@
66
<body>
77
<section>
88
<h2>Embabel Movie Finder</h2>
9-
<form action="/travel/journey/plan" method="post" th:object="${movieRequest}">
9+
<form action="/find-movies" method="post" th:object="${movieRequest}">
1010
<div class="form-group">
1111
<label for="brief">What I want</label>
1212
<textarea id="brief" name="brief" rows="3" cols="40" th:field="*{preference}" required></textarea>
1313
</div>
14-
15-
14+
1615
<button class="submit-btn" type="submit" style="margin-top: 20px;">Find Movies</button>
1716
</form>
18-
17+
1918
</section>
2019
</body>
2120
</html>

src/main/resources/templates/movie-recommendations.html

Lines changed: 9 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,67 +1,28 @@
11
<html xmlns:th="http://www.thymeleaf.org"
22
th:replace="~{common/layout :: layout(~{::title}, ~{::section})}">
33
<head>
4-
<title th:text="${travelPlan.proposal.title}">Embabel Journey Planner</title>
4+
<title th:text="${movieRecommendations.caption}">Embabel Movie Finder</title>
55
<th:block th:fragment="extraHeadContent">
66
<script src="https://unpkg.com/[email protected]"></script>
77
</th:block>
88
</head>
99
<body>
1010
<section>
11-
<div th:if="${travelPlan != null}">
12-
<h2 th:text="${travelPlan.proposal.title}"></h2>
11+
<div th:if="${movieRecommendations != null}">
12+
<h2 th:text="${movieRecommendations.caption}"></h2>
13+
14+
<p th:utext="${movieRecommendations.writeup}"></p>
15+
1316

1417
<div style="margin-bottom: 20px;">
15-
<a href="/travel/journey" target="_blank" rel="noopener noreferrer"
18+
<a href="/" target="_blank" rel="noopener noreferrer"
1619
class="friendly-map-btn">
17-
Plan Another Journey
20+
Look for more movies
1821
</a>
1922
</div>
20-
21-
22-
<div th:if="${travelPlan.brief != null}">
23-
<h3>Brief</h3>
24-
<p style="margin-bottom: 0.75rem;">From [[${travelPlan.brief.from}]] to [[${travelPlan.brief.to}]]
25-
from [[${#temporals.format(travelPlan.brief.departureDate, 'dd MMM yyyy')}]] to
26-
[[${#temporals.format(travelPlan.brief.returnDate, 'dd MMM yyyy')}]]</p>
27-
<i style="margin-bottom: 1rem; display:block;">[[${travelPlan.brief.brief}]]</i>
28-
29-
<ul class="travelers-list">
30-
<li th:each="traveler : ${travelPlan.travelers.travelers}">
31-
<strong th:text="${traveler.name}"></strong>
32-
<p th:text="${traveler.about}"></p>
33-
</li>
34-
</ul>
35-
36-
<h3>Plan</h3>
37-
<div style="margin-bottom: 20px;">
38-
<a th:href="${travelPlan.journeyMapUrl}" target="_blank" rel="noopener noreferrer"
39-
class="friendly-map-btn">
40-
Interactive Map
41-
</a>
42-
</div>
43-
44-
<div th:utext="${travelPlan.proposal.plan}" style="margin-bottom: 1rem;"></div>
45-
<h3>Stays</h3>
46-
<ul style="margin-bottom: 1rem;">
47-
<li th:each="stay : ${travelPlan.stays}" style="margin-bottom: 0.5rem;">
48-
<a th:href="${stay.airbnbUrl}" target="_blank" rel="noopener noreferrer"><span
49-
th:text="${stay.stayingAt() + ' on Airbnb for ' + stay.days.size() + ' nights'}"></span></a>
50-
</li>
51-
</ul>
52-
53-
<div th:if="${travelPlan.proposal.pageLinks != null && !#lists.isEmpty(travelPlan.proposal.pageLinks)}">
54-
<h3>Useful Links</h3>
55-
<ul style="margin-bottom: 1rem;">
56-
<li th:each="page : ${travelPlan.proposal.pageLinks}" style="margin-bottom: 0.5rem;">
57-
<a th:href="${page.url}" target="_blank" rel="noopener noreferrer"><span
58-
th:text="${page.summary}"></span></a>
59-
</li>
60-
</ul>
61-
</div>
62-
</div>
6323
</div>
6424

25+
6526
<hr class="dotted"/>
6627
<div id="currenxt-plan" th:replace="~{common/fragments/plan-complete :: plan-complete}"></div>
6728
</section>

0 commit comments

Comments
 (0)