initial commit
This commit is contained in:
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
""";
|
||||
}
|
||||
42
src/main/python/scrape-blog.py
Normal file
42
src/main/python/scrape-blog.py
Normal 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)
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
1
src/main/resources/META-INF/resources/fonts/red-hat-font.min.css
vendored
Normal file
1
src/main/resources/META-INF/resources/fonts/red-hat-font.min.css
vendored
Normal 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 */
|
||||
1399
src/main/resources/META-INF/resources/icons/font-awesome-solid.js
Normal file
1399
src/main/resources/META-INF/resources/icons/font-awesome-solid.js
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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';
|
||||
82
src/main/resources/META-INF/resources/index.html
Normal file
82
src/main/resources/META-INF/resources/index.html
Normal 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>
|
||||
9
src/main/resources/application.properties
Normal file
9
src/main/resources/application.properties
Normal 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
|
||||
Reference in New Issue
Block a user