mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-23 18:26:41 +02:00
feat: add unified @manacore/shared-llm package and migrate all backends
Create a shared LLM client package that provides a unified interface to the mana-llm service, replacing 9 individual fetch-based integrations with consistent error handling, retry logic, and JSON extraction. Package (@manacore/shared-llm): - LlmModule with forRoot/forRootAsync (NestJS dynamic module) - LlmClientService: chat, json, vision, visionJson, embed, stream - LlmClient standalone class for non-NestJS consumers - extractJson utility (consolidates 3 markdown-stripping implementations) - retryFetch with exponential backoff (429, 5xx, network errors) - 44 unit tests (json-extractor, retry, llm-client) Migrated backends: - mana-core-auth: raw fetch → llm.json() - planta: raw fetch + vision → llm.visionJson() - nutriphi: raw fetch + regex → llm.visionJson() + llm.json() - chat: custom OllamaService (175 LOC) → llm.chatMessages() - context: raw fetch → llm.chat() (keeps token tracking) - traces: 2x raw fetch → llm.chat() - manadeck: @google/genai SDK → llm.json() + llm.visionJson() - bot-services: raw Ollama API → LlmClient standalone - matrix-ollama-bot: raw fetch → llm.chatMessages() + llm.vision() New credit operations: - AI_PLANT_ANALYSIS (2 credits, planta) - AI_GUIDE_GENERATION (5 credits, traces) - AI_CONTEXT_GENERATION (2 credits, context) - AI_BOT_CHAT (0.1 credits, matrix) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
e7bf58c5b6
commit
e2f144962c
48 changed files with 2476 additions and 1297 deletions
|
|
@ -23,17 +23,18 @@
|
|||
"db:seed": "tsx src/db/seed.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@google/generative-ai": "^0.21.0",
|
||||
"@manacore/shared-error-tracking": "workspace:*",
|
||||
"@nutriphi/shared": "workspace:*",
|
||||
"@manacore/shared-llm": "workspace:^",
|
||||
"@manacore/shared-nestjs-auth": "workspace:*",
|
||||
"@manacore/shared-nestjs-health": "workspace:*",
|
||||
"@manacore/shared-nestjs-metrics": "workspace:*",
|
||||
"@manacore/shared-nestjs-setup": "workspace:*",
|
||||
"@google/generative-ai": "^0.21.0",
|
||||
"@nestjs/common": "^10.4.15",
|
||||
"@nestjs/config": "^3.3.0",
|
||||
"@nestjs/core": "^10.4.15",
|
||||
"@nestjs/platform-express": "^10.4.15",
|
||||
"@nutriphi/shared": "workspace:*",
|
||||
"class-transformer": "^0.5.1",
|
||||
"class-validator": "^0.14.1",
|
||||
"dotenv": "^16.4.7",
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { Injectable, OnModuleInit, Logger } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { LlmClientService } from '@manacore/shared-llm';
|
||||
import type { AIAnalysisResult } from '../types/nutrition.types';
|
||||
|
||||
const ANALYSIS_PROMPT = `Du bist ein Ernährungsexperte. Analysiere das Bild dieser Mahlzeit und liefere eine detaillierte Nährwertanalyse.
|
||||
|
|
@ -75,95 +75,28 @@ Antworte NUR mit einem validen JSON-Objekt im folgenden Format:
|
|||
}`;
|
||||
|
||||
@Injectable()
|
||||
export class GeminiService implements OnModuleInit {
|
||||
export class GeminiService {
|
||||
private readonly logger = new Logger(GeminiService.name);
|
||||
private manaLlmUrl: string | null = null;
|
||||
private readonly visionModel = 'ollama/llava:7b';
|
||||
private readonly textModel = 'ollama/gemma3:4b';
|
||||
|
||||
constructor(private configService: ConfigService) {}
|
||||
|
||||
onModuleInit() {
|
||||
this.manaLlmUrl = this.configService.get<string>('MANA_LLM_URL') || 'http://localhost:3025';
|
||||
this.logger.log(`NutriPhi AI using mana-llm at ${this.manaLlmUrl}`);
|
||||
}
|
||||
constructor(private readonly llm: LlmClientService) {}
|
||||
|
||||
async analyzeImage(imageBase64: string, mimeType = 'image/jpeg'): Promise<AIAnalysisResult> {
|
||||
if (!this.manaLlmUrl) {
|
||||
throw new Error('mana-llm not configured');
|
||||
}
|
||||
|
||||
const response = await fetch(`${this.manaLlmUrl}/v1/chat/completions`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
model: this.visionModel,
|
||||
messages: [
|
||||
{
|
||||
role: 'user',
|
||||
content: [
|
||||
{ type: 'text', text: ANALYSIS_PROMPT },
|
||||
{
|
||||
type: 'image_url',
|
||||
image_url: { url: `data:${mimeType};base64,${imageBase64}` },
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
temperature: 0.3,
|
||||
}),
|
||||
signal: AbortSignal.timeout(120000),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
this.logger.error(`mana-llm vision error: ${response.status} - ${errorText}`);
|
||||
throw new Error('Failed to analyze image');
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
const text = data.choices?.[0]?.message?.content || '';
|
||||
|
||||
// Extract JSON from response
|
||||
const jsonMatch = text.match(/\{[\s\S]*\}/);
|
||||
if (!jsonMatch) {
|
||||
throw new Error('Failed to parse AI response');
|
||||
}
|
||||
|
||||
return JSON.parse(jsonMatch[0]) as AIAnalysisResult;
|
||||
const { data } = await this.llm.visionJson<AIAnalysisResult>(
|
||||
ANALYSIS_PROMPT,
|
||||
imageBase64,
|
||||
mimeType,
|
||||
{ temperature: 0.3 }
|
||||
);
|
||||
return data;
|
||||
}
|
||||
|
||||
async analyzeText(description: string): Promise<AIAnalysisResult> {
|
||||
if (!this.manaLlmUrl) {
|
||||
throw new Error('mana-llm not configured');
|
||||
}
|
||||
|
||||
const prompt = TEXT_ANALYSIS_PROMPT.replace('{INPUT}', description);
|
||||
|
||||
const response = await fetch(`${this.manaLlmUrl}/v1/chat/completions`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
model: this.textModel,
|
||||
messages: [{ role: 'user', content: prompt }],
|
||||
temperature: 0.3,
|
||||
}),
|
||||
signal: AbortSignal.timeout(60000),
|
||||
const { data } = await this.llm.json<AIAnalysisResult>(prompt, {
|
||||
temperature: 0.3,
|
||||
timeout: 60_000,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`mana-llm error: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
const text = data.choices?.[0]?.message?.content || '';
|
||||
|
||||
// Extract JSON from response
|
||||
const jsonMatch = text.match(/\{[\s\S]*\}/);
|
||||
if (!jsonMatch) {
|
||||
throw new Error('Failed to parse AI response');
|
||||
}
|
||||
|
||||
return JSON.parse(jsonMatch[0]) as AIAnalysisResult;
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { Module } from '@nestjs/common';
|
||||
import { ConfigModule } from '@nestjs/config';
|
||||
import { ConfigModule, ConfigService } from '@nestjs/config';
|
||||
import { LlmModule } from '@manacore/shared-llm';
|
||||
import { DatabaseModule } from './db/database.module';
|
||||
import { HealthModule } from '@manacore/shared-nestjs-health';
|
||||
import { MetricsModule } from '@manacore/shared-nestjs-metrics';
|
||||
|
|
@ -16,6 +17,14 @@ import { RecommendationsModule } from './recommendations/recommendations.module'
|
|||
isGlobal: true,
|
||||
envFilePath: ['.env', '.env.development'],
|
||||
}),
|
||||
LlmModule.forRootAsync({
|
||||
imports: [ConfigModule],
|
||||
useFactory: (config: ConfigService) => ({
|
||||
manaLlmUrl: config.get('MANA_LLM_URL'),
|
||||
debug: config.get('NODE_ENV') === 'development',
|
||||
}),
|
||||
inject: [ConfigService],
|
||||
}),
|
||||
DatabaseModule,
|
||||
HealthModule.forRoot({ serviceName: 'nutriphi-backend' }),
|
||||
MetricsModule.register({
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue