Skip to content

Commit

Permalink
Plumb streaming variant for chat requests (#21)
Browse files Browse the repository at this point in the history
* Streaming support for chat interface
* Refine all views and layout
  * Accordion in navbar removed. Logo moved to navbar. Content pane left justified.
  * Intro Inquiry and MetadataFilter model objects. Such requests are now POST requests. Use Playtika's ReactiveFeign client.
* Fix CI and Release Github Actions. Fix serialization of metadata filter in streaming client.
  • Loading branch information
pacphi authored Dec 14, 2024
1 parent cf56ad4 commit c2133ac
Show file tree
Hide file tree
Showing 26 changed files with 299 additions and 306 deletions.
8 changes: 8 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,14 @@ jobs:
with:
distribution: liberica
java-version: ${{ matrix.java }}
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Install npm packages
run: |
npm install @vaadin/hilla-lit-form
npm install @vaadin/hilla-react-signals
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v4
- name: Build with Gradle
Expand Down
8 changes: 8 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,14 @@ jobs:
with:
distribution: liberica
java-version: 21
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Install npm packages
run: |
npm install @vaadin/hilla-lit-form
npm install @vaadin/hilla-react-signals
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v4
- name: Build artifact
Expand Down
5 changes: 3 additions & 2 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,13 @@ ext {

dependencies {
implementation 'org.springframework.boot:spring-boot-starter-actuator'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-webflux'
implementation 'org.springframework.cloud:spring-cloud-starter-openfeign'
implementation 'org.apache.commons:commons-collections4:4.4'
implementation 'io.github.openfeign:feign-hc5:13.5'
implementation 'com.playtika.reactivefeign:feign-reactor-spring-cloud-starter:4.2.1'
implementation 'com.vaadin:vaadin-spring-boot-starter'
implementation 'org.vaadin.olli:file-download-wrapper:7.1.0'
implementation 'org.commonmark:commonmark:0.24.0'
implementation 'org.apache.commons:commons-lang3'
implementation 'io.pivotal.cfenv:java-cfenv-all:3.3.0'
implementation 'org.springframework.cloud:spring-cloud-bindings:2.0.4'
Expand Down
2 changes: 2 additions & 0 deletions docs/BUILD.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
## How to Build

```bash
npm install @vaadin/hilla-lit-form
npm install @vaadin/hilla-react-signals
./gradlew clean build
```

Expand Down
22 changes: 22 additions & 0 deletions src/main/frontend/flow/markdown-component.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import React, {useEffect, useState} from 'react';
import {ReactAdapterElement} from 'Frontend/generated/flow/ReactAdapter';
import {effect, signal} from "@vaadin/hilla-react-signals";
import Markdown from "react-markdown";

class MarkdownElement extends ReactAdapterElement {

markdown = signal('');

protected override render() {
// In a React component, we could use the signal value directly,
// but it doesn't trigger an update in the ReactAdapterElement render method.
// Instead, pass the signal value to useState for React.
const [content, setContent] = useState('');
useEffect(() => effect(() => {
setContent(this.markdown.value);
}), []);
return <Markdown>{content}</Markdown>;
}
}

customElements.define('markdown-component', MarkdownElement);
4 changes: 4 additions & 0 deletions src/main/java/org/cftoolsuite/SanfordUiApplication.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package org.cftoolsuite;

import com.vaadin.flow.component.page.Push;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.ConfigurationPropertiesScan;
Expand All @@ -9,9 +10,12 @@
import com.vaadin.flow.server.PWA;
import com.vaadin.flow.theme.Theme;
import com.vaadin.flow.theme.lumo.Lumo;
import reactivefeign.spring.config.EnableReactiveFeignClients;

@Push
@SpringBootApplication
@EnableFeignClients
@EnableReactiveFeignClients
@ConfigurationPropertiesScan
@Theme(themeClass = Lumo.class, variant = Lumo.DARK)
@PWA(name = "A simple user interface to interact with a sanford instance", shortName = "sanford-ui")
Expand Down
11 changes: 4 additions & 7 deletions src/main/java/org/cftoolsuite/client/SanfordClient.java
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
package org.cftoolsuite.client;

import java.util.List;
import java.util.Map;

import org.cftoolsuite.domain.FileMetadata;
import org.cftoolsuite.domain.chat.Inquiry;
import org.cftoolsuite.domain.crawl.CrawlRequest;
import org.cftoolsuite.domain.crawl.CrawlResponse;
import org.cftoolsuite.domain.fetch.FetchRequest;
Expand All @@ -21,7 +21,7 @@
import org.springframework.web.bind.annotation.RequestPart;
import org.springframework.web.multipart.MultipartFile;

@FeignClient(name = "document-service", url = "${document.service.url}")
@FeignClient(name = "sanford-client", url = "${document.service.url}")
public interface SanfordClient {

@PostMapping(value = "/api/files/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
Expand All @@ -33,11 +33,8 @@ public interface SanfordClient {
@PostMapping("/api/fetch")
public ResponseEntity<FetchResponse> fetchUrls(@RequestBody FetchRequest request);

@GetMapping("/api/chat")
public ResponseEntity<String> chat(
@RequestParam("q") String message,
@RequestParam(value = "f", required = false) Map<String, Object> filterMetadata
);
@PostMapping("/api/chat")
public ResponseEntity<String> chat(@RequestBody Inquiry inquiry);

@GetMapping(value = "/api/files", produces = MediaType.APPLICATION_JSON_VALUE)
ResponseEntity<List<FileMetadata>> getFileMetadata(@RequestParam(value = "fileName", required = false) String fileName);
Expand Down
15 changes: 15 additions & 0 deletions src/main/java/org/cftoolsuite/client/SanfordStreamingClient.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package org.cftoolsuite.client;

import org.cftoolsuite.domain.chat.Inquiry;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import reactivefeign.spring.config.ReactiveFeignClient;
import reactor.core.publisher.Flux;

@ReactiveFeignClient(name="sanford-streaming-client", url="${document.service.url}")
public interface SanfordStreamingClient {

@PostMapping("/api/stream/chat")
public Flux<String> streamResponseToQuestion(@RequestBody Inquiry inquiry);
}

39 changes: 0 additions & 39 deletions src/main/java/org/cftoolsuite/config/OpenFeign.java

This file was deleted.

4 changes: 4 additions & 0 deletions src/main/java/org/cftoolsuite/domain/chat/FilterMetadata.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package org.cftoolsuite.domain.chat;

public record FilterMetadata(String key, Object value) {
}
6 changes: 6 additions & 0 deletions src/main/java/org/cftoolsuite/domain/chat/Inquiry.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package org.cftoolsuite.domain.chat;

import java.util.List;

public record Inquiry(String question, List<FilterMetadata> filter) {
}
17 changes: 11 additions & 6 deletions src/main/java/org/cftoolsuite/ui/MainLayout.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package org.cftoolsuite.ui;

import com.vaadin.flow.component.html.Image;
import com.vaadin.flow.server.StreamResource;
import org.cftoolsuite.ui.view.ChatView;
import org.cftoolsuite.ui.view.CrawlView;
import org.cftoolsuite.ui.view.DeleteView;
Expand Down Expand Up @@ -32,9 +34,6 @@ public class MainLayout extends AppLayout {
public MainLayout() {
Tab homeTab = createTab(VaadinIcon.HOME.create(), "Home", HomeView.class);

Accordion accordion = new Accordion();
accordion.setSizeFull();

Tabs actionTabs = createTabs();

Tab uploadTab = createTab(VaadinIcon.UPLOAD.create(), "Upload documents", UploadView.class);
Expand All @@ -47,10 +46,8 @@ public MainLayout() {
Tab downloadTab = createTab(VaadinIcon.DOWNLOAD.create(), "Download a document", DownloadView.class);
Tab deleteTab = createTab(VaadinIcon.TRASH.create(), "Delete a document", DeleteView.class);
actionTabs.add(uploadTab, crawlTab, fetchTab, chatTab, listTab, searchTab, summaryTab, downloadTab, deleteTab);
accordion.add("Actions", actionTabs).addThemeVariants(DetailsVariant.REVERSE);

addToNavbar(true, homeTab, new DrawerToggle());
addToDrawer(accordion);
addToDrawer(getLogoImage(), actionTabs);
}

private Tabs createTabs() {
Expand All @@ -77,4 +74,12 @@ private Tab createTab(Icon icon, String label, Class<? extends Component> layout
return tab;
}

private Image getLogoImage() {
StreamResource imageResource = new StreamResource("sanford.png",
() -> getClass().getResourceAsStream("/static/sanford.png"));
Image logo = new Image(imageResource, "Logo");
logo.setWidth("240px");
return logo;
}

}
36 changes: 25 additions & 11 deletions src/main/java/org/cftoolsuite/ui/component/Markdown.java
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
package org.cftoolsuite.ui.component;

import com.vaadin.flow.component.Composite;
import com.vaadin.flow.component.html.Div;
import org.commonmark.parser.Parser;
import org.commonmark.renderer.html.HtmlRenderer;
import com.vaadin.flow.component.Tag;
import com.vaadin.flow.component.dependency.JsModule;
import com.vaadin.flow.component.dependency.NpmPackage;
import com.vaadin.flow.component.react.ReactAdapterComponent;

public class Markdown extends Composite<Div> {
@NpmPackage(value = "react-markdown", version = "9.0.1")
@JsModule("./flow/markdown-component.tsx")
@Tag("markdown-component")
public class Markdown extends ReactAdapterComponent {

Parser parser = Parser.builder().build();
HtmlRenderer renderer = HtmlRenderer.builder().build();
private String markdown = "";

public Markdown() {
}
Expand All @@ -18,8 +20,20 @@ public Markdown(String markdown) {
}

public void setMarkdown(String markdown) {
getContent().getElement().setProperty("innerHTML",
renderer.render(parser.parse(markdown))
);
this.markdown = markdown;
getElement().executeJs("this.markdown.value = $0", markdown);
}
}

public void appendMarkdown(String additionalMarkdown) {
this.markdown += additionalMarkdown;
getElement().executeJs("this.markdown.value += $0", additionalMarkdown);
}

public void clear() {
setMarkdown("");
}

public String getMarkdown() {
return markdown;
}
}
28 changes: 13 additions & 15 deletions src/main/java/org/cftoolsuite/ui/component/MetadataFilter.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,11 @@
import com.vaadin.flow.component.orderedlayout.HorizontalLayout;
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
import com.vaadin.flow.component.textfield.TextField;
import org.cftoolsuite.domain.chat.FilterMetadata;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.*;

public class MetadataFilter extends CustomField<Map<String, Object>> {
public class MetadataFilter extends CustomField<List<FilterMetadata>> {
private final Grid<MetadataEntry> grid;
private final List<MetadataEntry> entries;
private final TextField keyField;
Expand All @@ -38,7 +36,7 @@ public MetadataFilter() {

HorizontalLayout inputLayout = new HorizontalLayout(keyField, valueField, addButton);
inputLayout.setWidth("100%");
inputLayout.setAlignItems(FlexComponent.Alignment.BASELINE);
inputLayout.setAlignItems(FlexComponent.Alignment.END);

VerticalLayout layout = new VerticalLayout(inputLayout, grid);
layout.setSpacing(false);
Expand Down Expand Up @@ -70,34 +68,34 @@ private void addEntry() {
}

@Override
protected Map<String, Object> generateModelValue() {
protected List<FilterMetadata> generateModelValue() {
if (entries.isEmpty()) {
return null;
}

Map<String, Object> metadata = new HashMap<>();
List<FilterMetadata> metadata = new ArrayList<>();
for (MetadataEntry entry : entries) {
metadata.put(entry.getKey(), entry.getValue());
metadata.add(new FilterMetadata(entry.getKey(), entry.getValue()));
}
return metadata;
}

@Override
public void setPresentationValue(Map<String, Object> metadata) {
public void setPresentationValue(List<FilterMetadata> metadata) {
entries.clear();
if (metadata != null) {
metadata.forEach((key, value) ->
entries.add(new MetadataEntry(key, value.toString()))
metadata.forEach(fm ->
entries.add(new MetadataEntry(fm.key(), fm.value()))
);
}
grid.getDataProvider().refreshAll();
}

private static class MetadataEntry {
private final String key;
private final String value;
private final Object value;

public MetadataEntry(String key, String value) {
public MetadataEntry(String key, Object value) {
this.key = key;
this.value = value;
}
Expand All @@ -106,7 +104,7 @@ public String getKey() {
return key;
}

public String getValue() {
public Object getValue() {
return value;
}
}
Expand Down
Loading

0 comments on commit c2133ac

Please sign in to comment.