Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,8 @@ embabel-agent-api/src/main/resources/mcp/**

.idea/workspace.xml

**/*.log
**/*.log

.idea/**

MovieFinder.iml
2 changes: 2 additions & 0 deletions .idea/compiler.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 5 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ An intelligent movie recommendation agent that analyzes taste profiles and sugge

## API keys you'll need

Besides your LLM keys such as as `OPENAI_API_KEY`, you will need to set the following environment variables:

- `export OMDB_API_KEY=<your_omdb_key>`
- `export X_RAPIDAPI_KEY=<your_rapidapi_key>`

Expand Down Expand Up @@ -73,14 +75,10 @@ First, start the PostgresSQL database using Docker with:
docker compose up
```

Now you can start the agent under Spring Shell using the shell script `./shell.sh` or run `FlickerApplication` in your
IDE.

Example command:
Start the application under your IDE or by navigating to the `scripts` directory and typing `./run.sh`.

```bash
x "Suggest movies for Rod tonight. He'd like to see a film set in mountains"
```
You will find the app at http://localhost:2001. Thanks to
Stanley Kubrick.

The logging will channel [Severance](https://www.imdb.com/title/tt11280740/).

Expand Down
8 changes: 4 additions & 4 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
services:
postgres:
image: pgvector/pgvector:pg15
image: pgvector/pgvector:0.8.0-pg17
ports:
- "5432:5432"
volumes:
- ~/apps/postgres/movie-finder:/var/lib/postgresql/data # Project-specific folder
- ~/apps/postgres/flicker:/var/lib/postgresql/data # Project-specific folder
- ./init.sql:/docker-entrypoint-initdb.d/init.sql
environment:
- POSTGRES_PASSWORD=${MF_PGPASSWORD:-look_to_the_stars}
- POSTGRES_USER=${MF_PGUSER:-embabel}
- POSTGRES_DB=movie-finder
- POSTGRES_USER=${MF_PGUSER:-flicker}
- POSTGRES_DB=flicker
10 changes: 6 additions & 4 deletions init.sql
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@
DO
$$
BEGIN
IF NOT EXISTS (SELECT FROM pg_catalog.pg_roles WHERE rolename = 'embabel') THEN
CREATE ROLE embabel WITH LOGIN PASSWORD 'look_to_the_stars';
IF NOT EXISTS (SELECT FROM pg_catalog.pg_roles WHERE rolname = 'flicker') THEN
CREATE ROLE flicker WITH LOGIN PASSWORD 'look_to_the_stars';
END IF;
END
$$;

GRANT ALL PRIVILEGES ON DATABASE "movie-finder" TO embabel;
ALTER USER embabel CREATEDB;
GRANT ALL PRIVILEGES ON DATABASE "flicker" TO flicker;
ALTER USER flicker CREATEDB;

CREATE SCHEMA IF NOT EXISTS flicker AUTHORIZATION flicker;
33 changes: 33 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -41,12 +41,45 @@
<scope>runtime</scope>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>

<dependency>
<groupId>com.embabel.agent</groupId>
<artifactId>embabel-agent-test</artifactId>
<version>${embabel-agent.version}</version>
<scope>test</scope>
</dependency>

<!-- Spring Security -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>

<!-- OAuth2 Client for Google Authentication -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>

<!-- Thymeleaf Spring Security integration -->
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-springsecurity6</artifactId>
</dependency>


<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>


<!-- Unit and Integration Testing -->
<dependency>
<groupId>org.springframework.boot</groupId>
Expand Down
File renamed without changes.
8 changes: 0 additions & 8 deletions scripts/shell.cmd

This file was deleted.

Empty file modified scripts/support/agent.sh
100644 → 100755
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package com.embabel.agent.web.htmx

import com.embabel.agent.core.AgentProcess
import org.springframework.ui.Model

/**
* Generic processing values to be used in the model for HTMX responses
* across different apps.
* This allows for consistent handling of agent processes and page details.
*/
data class GenericProcessingValues(
val agentProcess: AgentProcess,
val pageTitle: String,
val detail: String,
val resultModelKey: String,
val successView: String,
val css: String,
) {

fun addToModel(model: Model) {
model.addAttribute("processId", agentProcess.id)
model.addAttribute("pageTitle", pageTitle)
model.addAttribute("detail", detail)
model.addAttribute("resultModelKey", resultModelKey)
model.addAttribute("successView", successView)
model.addAttribute("css", css)
}
}
16 changes: 16 additions & 0 deletions src/main/kotlin/com/embabel/agent/web/htmx/PlatformController.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.embabel.agent.web.htmx

import org.springframework.stereotype.Controller
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RequestMapping

@Controller
@RequestMapping("/platform")
class PlatformController(
) {

@GetMapping
fun home(): String {
return "common/platform"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package com.embabel.agent.web.htmx

import com.embabel.agent.core.AgentPlatform
import com.embabel.agent.core.AgentProcessStatusCode
import org.slf4j.LoggerFactory
import org.springframework.http.HttpStatus
import org.springframework.stereotype.Controller
import org.springframework.ui.Model
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.RequestParam
import org.springframework.web.server.ResponseStatusException

@Controller
class ProcessStatusController(
private val agentPlatform: AgentPlatform,
) {

private val logger = LoggerFactory.getLogger(ProcessStatusController::class.java)

/**
* The HTML page that shows the status of the plan generation.
*/
@GetMapping("/status/{processId}")
fun checkPlanStatus(
@PathVariable processId: String,
@RequestParam resultModelKey: String,
@RequestParam successView: String,
@RequestParam css: String,
model: Model,
): String {
val agentProcess = agentPlatform.getAgentProcess(processId)
?: throw ResponseStatusException(HttpStatus.NOT_FOUND, "Process not found")

return when (agentProcess.status) {
AgentProcessStatusCode.COMPLETED -> {
logger.info("Process {} completed successfully", processId)
val lr = agentProcess.lastResult()
model.addAttribute(resultModelKey, lr)
model.addAttribute("css", css)
model.addAttribute("agentProcess", agentProcess)
successView
}

AgentProcessStatusCode.FAILED -> {
logger.error("Process {} failed: {}", processId, agentProcess.failureInfo)
model.addAttribute("error", "Failed to generate travel plan: ${agentProcess.failureInfo}")
"common/processing-error"
}

AgentProcessStatusCode.TERMINATED -> {
logger.info("Process {} was terminated", processId)
model.addAttribute("error", "Process was terminated before completion")
"common/processing-error"
}

else -> {
model.addAttribute("processId", processId)
model.addAttribute("pageTitle", "Processing...")
"common/processing" // Keep showing loading state
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package com.embabel.agent.web.security

import com.embabel.agent.domain.library.Person
import org.springframework.security.core.GrantedAuthority
import org.springframework.security.core.authority.SimpleGrantedAuthority
import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService
import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest
import org.springframework.security.oauth2.core.OAuth2AuthenticationException
import org.springframework.security.oauth2.core.user.OAuth2User
import org.springframework.stereotype.Service

@Service
class EmbabelOAuth2UserService(
private val userRepository: UserRepository,
) : DefaultOAuth2UserService() {

override fun loadUser(userRequest: OAuth2UserRequest): OAuth2User {
val user = super.loadUser(userRequest)

// Extract email from Google OAuth2 response
val email = user.attributes["email"] as? String
?: throw OAuth2AuthenticationException("Email not found in OAuth2 response")

// Check if user is authorized in your database
val embabelUser = userRepository.findByEmail(email)
?: throw OAuth2AuthenticationException("User not authorized: $email")

// if (!authorizedUser.isActive) {
// throw OAuth2AuthenticationException("User account is inactive: $email")
// }

// val authorities = authorizedUser.roles.map {
// SimpleGrantedAuthority("ROLE_${it.name}")
// }

val authorities = listOf(SimpleGrantedAuthority("ROLE_USER"))

return EmbabelAuth2User(
embabelUser,
user.attributes,
authorities,
)
}
}

interface User : Person {
val email: String
}

interface UserRepository {
fun findByEmail(email: String): User?
}

class EmbabelAuth2User(
private val user: User,
private val attributes: Map<String, Any>,
private val authorities: Collection<GrantedAuthority>
) : OAuth2User {

override fun getName(): String = user.name

override fun getAttributes(): Map<String, Any> = attributes

override fun getAuthorities(): Collection<GrantedAuthority> = authorities

fun getUser(): User = user
}
14 changes: 14 additions & 0 deletions src/main/kotlin/com/embabel/agent/web/security/LoginController.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.embabel.agent.web.security

import org.springframework.stereotype.Controller
import org.springframework.web.bind.annotation.GetMapping

@Controller
class LoginController {

@GetMapping("/login")
fun login(): String {
return "login"
}

}
Loading
Loading