mirror of
https://github.com/BookStackApp/BookStack.git
synced 2025-10-20 20:12:39 +03:00
Vectors: Updated query response to use server-side-events
Allowing the vector query results and the LLM response to each come back over the same HTTP request at two different times via a somewhat standard. Uses a package for JS SSE client, since native browser client does not support over POST, which is probably important for this endpoint as we don't want crawlers or other bots abusing this via accidentally.
This commit is contained in:
@@ -41,8 +41,13 @@ class QueryController extends Controller
|
||||
// TODO - Validate if query system is active
|
||||
$query = $request->get('query', '');
|
||||
|
||||
$results = $query ? $searchRunner->run($query) : [];
|
||||
$llmResult = $llmRunner->run($query, $results);
|
||||
dd($results, $llmResult);
|
||||
return response()->eventStream(function () use ($query, $searchRunner, $llmRunner) {
|
||||
$results = $query ? $searchRunner->run($query) : [];
|
||||
|
||||
$count = count($results);
|
||||
yield "Found {$count} results for query: {$query}!";
|
||||
$llmResult = $llmRunner->run($query, $results);
|
||||
yield "LLM result: {$llmResult}";
|
||||
});
|
||||
}
|
||||
}
|
||||
|
22
package-lock.json
generated
22
package-lock.json
generated
@@ -23,6 +23,7 @@
|
||||
"@ssddanbrown/codemirror-lang-twig": "^1.0.0",
|
||||
"@types/jest": "^29.5.14",
|
||||
"codemirror": "^6.0.1",
|
||||
"eventsource-client": "^1.1.4",
|
||||
"idb-keyval": "^6.2.1",
|
||||
"markdown-it": "^14.1.0",
|
||||
"markdown-it-task-lists": "^2.1.1",
|
||||
@@ -4336,6 +4337,27 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/eventsource-client": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/eventsource-client/-/eventsource-client-1.1.4.tgz",
|
||||
"integrity": "sha512-CKnqZTwXCnHN2EqrEB9eLSjMMRqHum09VOsikkgSPoa2Jr2XgQnX7P1Fxhnnj/UHxi3GQ2xVsXDKIktEes07bg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"eventsource-parser": "^3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/eventsource-parser": {
|
||||
"version": "3.0.5",
|
||||
"resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.5.tgz",
|
||||
"integrity": "sha512-bSRG85ZrMdmWtm7qkF9He9TNRzc/Bm99gEJMaQoHJ9E6Kv9QBbsldh2oMj7iXmYNEAVvNgvv5vPorG6W+XtBhQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/execa": {
|
||||
"version": "5.1.1",
|
||||
"resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz",
|
||||
|
@@ -55,6 +55,7 @@
|
||||
"@ssddanbrown/codemirror-lang-twig": "^1.0.0",
|
||||
"@types/jest": "^29.5.14",
|
||||
"codemirror": "^6.0.1",
|
||||
"eventsource-client": "^1.1.4",
|
||||
"idb-keyval": "^6.2.1",
|
||||
"markdown-it": "^14.1.0",
|
||||
"markdown-it-task-lists": "^2.1.1",
|
||||
|
@@ -1,4 +1,5 @@
|
||||
import {Component} from "./component";
|
||||
import {createEventSource} from "eventsource-client";
|
||||
|
||||
export class QueryManager extends Component {
|
||||
protected input!: HTMLTextAreaElement;
|
||||
@@ -21,5 +22,36 @@ export class QueryManager extends Component {
|
||||
// TODO - Update URL on query change
|
||||
|
||||
// TODO - Handle query form submission
|
||||
this.form.addEventListener('submit', event => {
|
||||
event.preventDefault();
|
||||
this.runQuery();
|
||||
});
|
||||
}
|
||||
|
||||
async runQuery() {
|
||||
this.contentLoading.hidden = false;
|
||||
this.generatedLoading.hidden = false;
|
||||
this.contentDisplay.innerHTML = '';
|
||||
this.generatedDisplay.innerHTML = '';
|
||||
|
||||
const query = this.input.value;
|
||||
const es = window.$http.eventSource('/query', 'POST', {query});
|
||||
|
||||
let messageCount = 0;
|
||||
for await (const {data, event, id} of es) {
|
||||
messageCount++;
|
||||
if (messageCount === 1) {
|
||||
// Entity results
|
||||
this.contentDisplay.innerText = data; // TODO - Update to HTML
|
||||
this.contentLoading.hidden = true;
|
||||
} else if (messageCount === 2) {
|
||||
// LLM Output
|
||||
this.generatedDisplay.innerText = data; // TODO - Update to HTML
|
||||
this.generatedLoading.hidden = true;
|
||||
} else {
|
||||
es.close()
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,3 +1,5 @@
|
||||
import {createEventSource, EventSourceClient} from "eventsource-client";
|
||||
|
||||
type ResponseData = Record<any, any>|string;
|
||||
|
||||
type RequestOptions = {
|
||||
@@ -59,7 +61,6 @@ export class HttpManager {
|
||||
}
|
||||
|
||||
createXMLHttpRequest(method: string, url: string, events: Record<string, (e: Event) => void> = {}): XMLHttpRequest {
|
||||
const csrfToken = document.querySelector('meta[name=token]')?.getAttribute('content');
|
||||
const req = new XMLHttpRequest();
|
||||
|
||||
for (const [eventName, callback] of Object.entries(events)) {
|
||||
@@ -68,7 +69,7 @@ export class HttpManager {
|
||||
|
||||
req.open(method, url);
|
||||
req.withCredentials = true;
|
||||
req.setRequestHeader('X-CSRF-TOKEN', csrfToken || '');
|
||||
req.setRequestHeader('X-CSRF-TOKEN', this.getCSRFToken());
|
||||
|
||||
return req;
|
||||
}
|
||||
@@ -95,12 +96,11 @@ export class HttpManager {
|
||||
requestUrl = urlObj.toString();
|
||||
}
|
||||
|
||||
const csrfToken = document.querySelector('meta[name=token]')?.getAttribute('content') || '';
|
||||
const requestOptions: RequestInit = {...options, credentials: 'same-origin'};
|
||||
requestOptions.headers = {
|
||||
...requestOptions.headers || {},
|
||||
baseURL: window.baseUrl(''),
|
||||
'X-CSRF-TOKEN': csrfToken,
|
||||
'X-CSRF-TOKEN': this.getCSRFToken(),
|
||||
};
|
||||
|
||||
const response = await fetch(requestUrl, requestOptions);
|
||||
@@ -191,6 +191,27 @@ export class HttpManager {
|
||||
return this.dataRequest('DELETE', url, data);
|
||||
}
|
||||
|
||||
eventSource(url: string, method: string = 'GET', body: object = {}): EventSourceClient {
|
||||
if (!url.startsWith('http')) {
|
||||
url = window.baseUrl(url);
|
||||
}
|
||||
|
||||
return createEventSource({
|
||||
url,
|
||||
method,
|
||||
body: JSON.stringify(body),
|
||||
credentials: 'same-origin',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRF-TOKEN': this.getCSRFToken(),
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected getCSRFToken(): string {
|
||||
return document.querySelector('meta[name=token]')?.getAttribute('content') || '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the response text for an error response to a user
|
||||
* presentable string. Handles a range of errors responses including
|
||||
|
Reference in New Issue
Block a user