initial commit

This commit is contained in:
Markus Hofstetter
2025-04-24 22:16:02 +02:00
commit f9705f3e80
18 changed files with 2569 additions and 0 deletions

View File

@@ -0,0 +1,18 @@
package dev.langchain4j.quarkus.workshop;
import dev.langchain4j.service.SystemMessage;
import io.quarkiverse.langchain4j.RegisterAiService;
import io.smallrye.mutiny.Multi;
import jakarta.enterprise.context.SessionScoped;
@SessionScoped
@RegisterAiService
public interface CustomerSupportAgent {
@SystemMessage("""
You are a customer support agent of a car rental company 'Miles of Smiles'.
You are friendly, polite and concise.
If the question is unrelated to car rental, you should politely redirect the customer to the right department.
""")
Multi<String> chat(String userMessage);
}

View File

@@ -0,0 +1,26 @@
package dev.langchain4j.quarkus.workshop;
import io.quarkus.websockets.next.OnOpen;
import io.quarkus.websockets.next.OnTextMessage;
import io.quarkus.websockets.next.WebSocket;
import io.smallrye.mutiny.Multi;
@WebSocket(path = "/customer-support-agent")
public class CustomerSupportAgentWebSocket {
private final CustomerSupportAgent customerSupportAgent;
public CustomerSupportAgentWebSocket(CustomerSupportAgent customerSupportAgent) {
this.customerSupportAgent = customerSupportAgent;
}
@OnOpen
public String onOpen() {
return "Welcome to Miles of Smiles! How can I help you today?";
}
@OnTextMessage
public Multi<String> onTextMessage(String message) {
return customerSupportAgent.chat(message);
}
}

View File

@@ -0,0 +1,51 @@
package dev.langchain4j.quarkus.workshop;
import jakarta.annotation.PostConstruct;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import org.mvnpm.importmap.Aggregator;
/**
* Dynamically create the import map
*/
@ApplicationScoped
@Path("/_importmap")
public class ImportmapResource {
private String importmap;
// See https://github.com/WICG/import-maps/issues/235
// This does not seem to be supported by browsers yet...
@GET
@Path("/dynamic.importmap")
@Produces("application/importmap+json")
public String importMap() {
return this.importmap;
}
@GET
@Path("/dynamic-importmap.js")
@Produces("application/javascript")
public String importMapJson() {
return JAVASCRIPT_CODE.formatted(this.importmap);
}
@PostConstruct
void init() {
Aggregator aggregator = new Aggregator();
// Add our own mappings
aggregator.addMapping("icons/", "/icons/");
aggregator.addMapping("components/", "/components/");
aggregator.addMapping("fonts/", "/fonts/");
this.importmap = aggregator.aggregateAsJson();
}
private static final String JAVASCRIPT_CODE = """
const im = document.createElement('script');
im.type = 'importmap';
im.textContent = JSON.stringify(%s);
document.currentScript.after(im);
""";
}

View File

@@ -0,0 +1,42 @@
import requests
from bs4 import BeautifulSoup
from urllib.parse import urljoin
import os
base_url = "https://blog.vacme.ch/hemed/"
visited = set()
def scrape_page(url):
if url in visited:
return
visited.add(url)
try:
response = requests.get(url, timeout=10)
response.raise_for_status()
# Save HTML
filename = url.replace(base_url, "").replace("/", "_") or "index"
with open(f"vacme_blog/{filename}.html", "w", encoding="utf-8") as f:
f.write(response.text)
# Convert to Markdown (requires markdownify)
from markdownify import markdownify as md
with open(f"vacme_blog/{filename}.md", "w", encoding="utf-8") as f:
f.write(md(response.text))
# Find links recursively
soup = BeautifulSoup(response.text, "html.parser")
for link in soup.find_all("a", href=True):
new_url = urljoin(base_url, link["href"])
if new_url.startswith(base_url):
scrape_page(new_url)
except Exception as e:
print(f"Error scraping {url}: {str(e)}")
# Create output directory
os.makedirs("vacme_blog", exist_ok=True)
# Start scraping
scrape_page(base_url)

View File

@@ -0,0 +1,74 @@
import {css, LitElement} from 'lit';
import '@vaadin/icon';
import '@vaadin/button';
import '@vaadin/text-field';
import '@vaadin/text-area';
import '@vaadin/form-layout';
import '@vaadin/progress-bar';
import '@vaadin/checkbox';
import '@vaadin/horizontal-layout';
import '@vaadin/grid';
import '@vaadin/grid/vaadin-grid-sort-column.js';
export class DemoChat extends LitElement {
_stripHtml(html) {
const div = document.createElement("div");
div.innerHTML = html;
return div.textContent || div.innerText || "";
}
connectedCallback() {
const chatBot = document.getElementsByTagName("chat-bot")[0];
const protocol = (window.location.protocol === 'https:') ? 'wss' : 'ws';
const socket = new WebSocket(protocol + '://' + window.location.host + '/customer-support-agent');
const that = this;
socket.onmessage = function (event) {
chatBot.hideLastLoading();
// LLM response
let lastMessage;
if (chatBot.messages.length > 0) {
lastMessage = chatBot.messages[chatBot.messages.length - 1];
}
if (lastMessage && lastMessage.sender.name === "Bot" && ! lastMessage.loading) {
if (! lastMessage.msg) {
lastMessage.msg = "";
}
lastMessage.msg += event.data;
let bubbles = chatBot.shadowRoot.querySelectorAll("chat-bubble");
let bubble = bubbles.item(bubbles.length - 1);
if (lastMessage.message) {
bubble.innerHTML = that._stripHtml(lastMessage.message) + lastMessage.msg;
} else {
bubble.innerHTML = lastMessage.msg;
}
chatBot.body.scrollTo({ top: chatBot.body.scrollHeight, behavior: 'smooth' })
} else {
chatBot.sendMessage(event.data, {
right: false,
sender: {
name: "Bot"
}
});
}
}
chatBot.addEventListener("sent", function (e) {
if (e.detail.message.sender.name !== "Bot") {
// User message
const msg = that._stripHtml(e.detail.message.message);
socket.send(msg);
chatBot.sendMessage("", {
right: false,
loading: true
});
}
});
}
}
customElements.define('demo-chat', DemoChat);

View File

@@ -0,0 +1,63 @@
import {LitElement, html, css} from 'lit';
import '@vaadin/icon';
import '@vaadin/button';
import '@vaadin/text-field';
import '@vaadin/text-area';
import '@vaadin/form-layout';
import '@vaadin/progress-bar';
import '@vaadin/checkbox';
import '@vaadin/grid';
import '@vaadin/grid/vaadin-grid-sort-column.js';
export class DemoTitle extends LitElement {
static styles = css`
h2 {
font-family: "Red Hat Mono", monospace;
font-size: 60px;
font-style: normal;
font-variant: normal;
font-weight: 700;
line-height: 26.4px;
color: var(--main-highlight-text-color);
}
.title {
text-align: center;
padding: 1em;
background: var(--main-bg-color);
}
.explanation {
margin-left: auto;
margin-right: auto;
width: 50%;
text-align: justify;
font-size: 20px;
}
.explanation img {
max-width: 60%;
display: block;
float:left;
margin-right: 2em;
margin-top: 1em;
}
`
render() {
return html`
<div class="title">
<h2>Miles of Smiles</h2>
</div>
<div class="explanation">
<p>Welcome to Miles of Smiles!</p>
<p>Please click the button on the bottom right to start the conversation
with an LLM-powered customer support agent.</p>
</div>
`
}
}
customElements.define('demo-title', DemoTitle);

View File

@@ -0,0 +1 @@
@font-face{font-family:"Red Hat Display";font-style:normal;font-weight:400;font-display:swap;src:url(https://fonts.gstatic.com/s/redhatdisplay/v7/8vIQ7wUr0m80wwYf0QCXZzYzUoTg8z6hR4jNCH5Z.woff2) format("woff2");unicode-range:U+0100-024F,U+0259,U+1E00-1EFF,U+2020,U+20A0-20AB,U+20AD-20CF,U+2113,U+2C60-2C7F,U+A720-A7FF}@font-face{font-family:"Red Hat Display";font-style:normal;font-weight:400;font-display:swap;src:url(https://fonts.gstatic.com/s/redhatdisplay/v7/8vIQ7wUr0m80wwYf0QCXZzYzUoTg_T6hR4jNCA.woff2) format("woff2");unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+2000-206F,U+2074,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}@font-face{font-family:"Red Hat Display";font-style:normal;font-weight:500;font-display:swap;src:url(https://fonts.gstatic.com/s/redhatdisplay/v7/8vIQ7wUr0m80wwYf0QCXZzYzUoTg8z6hR4jNCH5Z.woff2) format("woff2");unicode-range:U+0100-024F,U+0259,U+1E00-1EFF,U+2020,U+20A0-20AB,U+20AD-20CF,U+2113,U+2C60-2C7F,U+A720-A7FF}@font-face{font-family:"Red Hat Display";font-style:normal;font-weight:500;font-display:swap;src:url(https://fonts.gstatic.com/s/redhatdisplay/v7/8vIQ7wUr0m80wwYf0QCXZzYzUoTg_T6hR4jNCA.woff2) format("woff2");unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+2000-206F,U+2074,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}@font-face{font-family:"Red Hat Display";font-style:normal;font-weight:700;font-display:swap;src:url(https://fonts.gstatic.com/s/redhatdisplay/v7/8vIQ7wUr0m80wwYf0QCXZzYzUoTg8z6hR4jNCH5Z.woff2) format("woff2");unicode-range:U+0100-024F,U+0259,U+1E00-1EFF,U+2020,U+20A0-20AB,U+20AD-20CF,U+2113,U+2C60-2C7F,U+A720-A7FF}@font-face{font-family:"Red Hat Display";font-style:normal;font-weight:700;font-display:swap;src:url(https://fonts.gstatic.com/s/redhatdisplay/v7/8vIQ7wUr0m80wwYf0QCXZzYzUoTg_T6hR4jNCA.woff2) format("woff2");unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+2000-206F,U+2074,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}@font-face{font-family:"Red Hat Text";font-style:normal;font-weight:400;font-display:swap;src:url(https://fonts.gstatic.com/s/redhattext/v6/RrQXbohi_ic6B3yVSzGBrMxQZqctMc-JPWCN.woff2) format("woff2");unicode-range:U+0100-024F,U+0259,U+1E00-1EFF,U+2020,U+20A0-20AB,U+20AD-20CF,U+2113,U+2C60-2C7F,U+A720-A7FF}@font-face{font-family:"Red Hat Text";font-style:normal;font-weight:400;font-display:swap;src:url(https://fonts.gstatic.com/s/redhattext/v6/RrQXbohi_ic6B3yVSzGBrMxQaKctMc-JPQ.woff2) format("woff2");unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+2000-206F,U+2074,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}@font-face{font-family:"Red Hat Text";font-style:normal;font-weight:500;font-display:swap;src:url(https://fonts.gstatic.com/s/redhattext/v6/RrQXbohi_ic6B3yVSzGBrMxQZqctMc-JPWCN.woff2) format("woff2");unicode-range:U+0100-024F,U+0259,U+1E00-1EFF,U+2020,U+20A0-20AB,U+20AD-20CF,U+2113,U+2C60-2C7F,U+A720-A7FF}@font-face{font-family:"Red Hat Text";font-style:normal;font-weight:500;font-display:swap;src:url(https://fonts.gstatic.com/s/redhattext/v6/RrQXbohi_ic6B3yVSzGBrMxQaKctMc-JPQ.woff2) format("woff2");unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+2000-206F,U+2074,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}/*# sourceMappingURL=red-hat-font.css.map */

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,7 @@
// import './font-awesome-brands.js';
// import './font-awesome-regular.js';
import './font-awesome-solid.js';
// export * from './font-awesome-brands.js';
// export * from './font-awesome-regular.js';
export * from './font-awesome-solid.js';

View File

@@ -0,0 +1,82 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<link rel="shortcut icon" type="image/png" href="favicon.ico">
<script src="/_importmap/dynamic-importmap.js"></script>
<script type="module">
import 'icons/font-awesome.js';
import 'components/demo-title.js';
import 'components/demo-chat.js';
import 'wc-chatbot';
</script>
<link rel="stylesheet" href="fonts/red-hat-font.min.css">
<title>Miles of Smiles</title>
<style>
:root {
--main-bg-color: rgb(246, 242, 242);
--main-highlight-text-color: rgba(237, 98, 128);
}
body {
margin: 0;
width: 100%;
height: 100vh;
font-family: 'Red Hat Text', sans-serif;
overflow: hidden;
display: flex;
flex-direction: column;
justify-content: center;
color: var(--lumo-body-text-color);
background: var(--main-bg-color);
}
chat-bot {
--chatbot-avatar-bg-color: rgba(237, 98, 128);
--chatbot-avatar-margin: 10%;
--chatbot-header-bg-color: rgba(237, 98, 128);
--chatbot-header-title-color: #FFFFFF;
--chatbot-body-bg-color: var(--main-bg-color);
--chatbot-send-button-color: rgba(237, 98, 128);
}
chat-bot::part(chat-bubble) {
--chat-bubble-avatar-color: rgba(237, 98, 128);
--chat-bubble-color: rgba(203, 232, 237, 0.71);
--chat-bubble-right-color: rgb(157, 238, 244);
--chat-bubble-font-color: #333;
--chat-bubble-font-right-color: #333;
--chat-bubble-delay: 0.2s;
}
.middle {
margin-top: 2em;
overflow: hidden;
width: 50%;
margin-left: auto;
margin-right: auto;
display: flex;
}
</style>
</head>
<body>
<demo-title></demo-title>
<div class="middle">
<demo-chat>
<chat-bot></chat-bot>
</demo-chat>
</div>
</body>
</html>

View File

@@ -0,0 +1,9 @@
quarkus.langchain4j.openai.api-key=${OPENAI_API_KEY}
quarkus.langchain4j.openai.chat-model.model-name=gpt-4o
quarkus.langchain4j.openai.chat-model.log-requests=true
quarkus.langchain4j.openai.chat-model.log-responses=true
quarkus.langchain4j.openai.chat-model.temperature=1.0
quarkus.langchain4j.openai.chat-model.max-tokens=1000
quarkus.langchain4j.openai.chat-model.frequency-penalty=0