1 Commits

Author SHA1 Message Date
dfe05cf2d2 Correction d'un bug lors de tentative de téléchargement de llm pour ollama
Some checks failed
E2E Tests / e2e (push) Failing after 19s
Build & Push Images / build (brain) (push) Successful in 50s
Build & Push Images / build (core) (push) Successful in 1m22s
Build & Push Images / build (web) (push) Successful in 1m28s
2026-04-26 01:45:39 +02:00
6 changed files with 52 additions and 19 deletions

View File

@@ -14,7 +14,7 @@
<groupId>com.loremind</groupId>
<artifactId>loremind-core</artifactId>
<version>0.6.9</version>
<version>0.6.10</version>
<name>LoreMind Core</name>
<description>Backend Core - Architecture Hexagonale</description>

View File

@@ -21,12 +21,10 @@ import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBo
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.Map;
@@ -46,13 +44,16 @@ public class SettingsController {
private final RestTemplate restTemplate;
private final String brainBaseUrl;
private final String brainInternalSecret;
private final boolean demoMode;
public SettingsController(RestTemplate restTemplate,
@Value("${brain.base-url}") String brainBaseUrl,
@Value("${brain.internal-secret}") String brainInternalSecret,
@Value("${app.demo-mode:false}") boolean demoMode) {
this.restTemplate = restTemplate;
this.brainBaseUrl = brainBaseUrl;
this.brainInternalSecret = brainInternalSecret;
this.demoMode = demoMode;
}
@@ -93,12 +94,18 @@ public class SettingsController {
HttpClient http = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(10))
.build();
HttpRequest req = HttpRequest.newBuilder()
// Le RestTemplate auto-injecte X-Internal-Secret via un interceptor,
// mais on bypass RestTemplate pour le streaming -> on doit ajouter
// l'entete a la main, sinon le Brain repond 401.
HttpRequest.Builder reqBuilder = HttpRequest.newBuilder()
.uri(URI.create(brainBaseUrl + "/models/ollama/pull"))
.timeout(Duration.ofMinutes(60))
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(toJson(body)))
.build();
.POST(HttpRequest.BodyPublishers.ofString(toJson(body)));
if (brainInternalSecret != null && !brainInternalSecret.isBlank()) {
reqBuilder.header("X-Internal-Secret", brainInternalSecret);
}
HttpRequest req = reqBuilder.build();
try {
HttpResponse<InputStream> resp = http.send(req, HttpResponse.BodyHandlers.ofInputStream());
try (InputStream in = resp.body()) {

View File

@@ -40,7 +40,7 @@
Auteur : ietm64
Licence : AGPL-3.0
Projet : LoreMindMJ - assistant pour Maitres de Jeu de JDR
Version : 0.6.9
Version : 0.6.10
.LINK
https://git.igmlcreation.fr/ietm64/loremind

4
web/package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "loremind-web",
"version": "0.6.9",
"version": "0.6.10",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "loremind-web",
"version": "0.6.9",
"version": "0.6.10",
"dependencies": {
"@angular/animations": "^17.0.0",
"@angular/common": "^17.0.0",

View File

@@ -1,6 +1,6 @@
{
"name": "loremind-web",
"version": "0.6.9",
"version": "0.6.10",
"description": "LoreMind Frontend - Angular",
"scripts": {
"ng": "ng",

View File

@@ -1,4 +1,4 @@
import { Injectable } from '@angular/core';
import { Injectable, NgZone } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
@@ -43,7 +43,7 @@ export class SettingsService {
// suivants en cross-origin (dev Angular sur :4200 -> core sur :8080).
private readonly authOptions = { withCredentials: true };
constructor(private http: HttpClient) {}
constructor(private http: HttpClient, private zone: NgZone) {}
getSettings(): Observable<AppSettings> {
return this.http.get<AppSettings>(this.apiUrl, this.authOptions);
@@ -78,8 +78,16 @@ export class SettingsService {
* bufferise les reponses XHR, ce qui empeche le streaming en temps reel.
*/
pullOllamaModel(name: string): Observable<OllamaPullEvent> {
// fetch() s'execute hors zone Angular -> les emissions du subscriber
// doivent etre forcees dans la zone via NgZone.run() pour que le change
// detection se declenche et que la barre de progression se mette a jour
// en temps reel.
return new Observable<OllamaPullEvent>((subscriber) => {
const controller = new AbortController();
const emit = (ev: OllamaPullEvent) => this.zone.run(() => subscriber.next(ev));
const fail = (e: unknown) => this.zone.run(() => subscriber.error(e));
const done = () => this.zone.run(() => subscriber.complete());
(async () => {
try {
const response = await fetch(`${this.apiUrl}/models/ollama/pull`, {
@@ -89,16 +97,22 @@ export class SettingsService {
body: JSON.stringify({ name }),
signal: controller.signal,
});
if (!response.ok || !response.body) {
subscriber.error(new Error(`HTTP ${response.status}`));
if (!response.ok) {
const text = await response.text();
fail(new Error(`HTTP ${response.status} : ${text || 'reponse vide'}`));
return;
}
if (!response.body) {
fail(new Error('Reponse sans corps : streaming non supporte par le navigateur'));
return;
}
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = '';
let eventsReceived = 0;
while (true) {
const { value, done } = await reader.read();
if (done) break;
const { value, done: streamDone } = await reader.read();
if (streamDone) break;
buffer += decoder.decode(value, { stream: true });
// Decoupage NDJSON : chaque ligne est un objet JSON complet.
let nl: number;
@@ -107,15 +121,27 @@ export class SettingsService {
buffer = buffer.slice(nl + 1);
if (!line) continue;
try {
subscriber.next(JSON.parse(line) as OllamaPullEvent);
emit(JSON.parse(line) as OllamaPullEvent);
eventsReceived++;
} catch {
// Ligne non JSON (rare) : on l'ignore.
}
}
}
subscriber.complete();
// Reste eventuel dans le buffer (derniere ligne sans '\n').
if (buffer.trim()) {
try {
emit(JSON.parse(buffer.trim()) as OllamaPullEvent);
eventsReceived++;
} catch { /* ignore */ }
}
if (eventsReceived === 0) {
fail(new Error('Aucun evenement recu du serveur. Verifiez les logs du Brain (docker logs loremind-brain).'));
return;
}
done();
} catch (err) {
if ((err as Error).name !== 'AbortError') subscriber.error(err);
if ((err as Error).name !== 'AbortError') fail(err);
}
})();
return () => controller.abort();