feat(backend): wire Ollama LLM for context messages (TODO-005)
Dual-path LLM enrichment for AI context prep messages: 1. extraction-service (if EXTRACTION_SERVICE_URL set) 2. Ollama direct (if OLLAMA_URL set) — non-streaming /api/generate 3. Keyword rules fallback 4. Generic fallback New env vars: OLLAMA_URL, OLLAMA_MODEL (default: gemma3:4b) Both LLM paths use 5s timeout and null-return-on-error pattern. Feature-gated behind ai_context_messages.enabled flag.
This commit is contained in:
parent
0e7c1aeb15
commit
229ce4f00f
@ -56,25 +56,13 @@ export interface ContextMessageResult {
|
|||||||
source: 'llm' | 'keyword' | 'generic';
|
source: 'llm' | 'keyword' | 'generic';
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── LLM enrichment (extraction-service or ollama-client) ──
|
// ── LLM enrichment (dual path: extraction-service or Ollama) ──
|
||||||
|
|
||||||
// TODO-005: Wire real LLM enrichment for context messages
|
async function llmViaExtraction(prompt: string): Promise<string | null> {
|
||||||
// Priority: high | Phase: A.4
|
|
||||||
// Replace the stub below with a real LLM call. Two options:
|
|
||||||
// Option A: @bytelyst/extraction client — POST to extraction-service /api/extract
|
|
||||||
// with task='timer-context'. Requires creating a new extraction task type in
|
|
||||||
// learning_ai_common_plat/services/extraction-service/src/modules/extract/.
|
|
||||||
// Option B: @bytelyst/ollama-client — call Ollama directly with buildPrompt().
|
|
||||||
// Simpler, no extraction-service dependency, but no task abstraction.
|
|
||||||
// The prompt is already built by buildPrompt() below. Just replace the fetch stub
|
|
||||||
// with a real client call. Keep the 5s timeout and null-return-on-error pattern.
|
|
||||||
async function llmEnrich(input: ContextMessageInput): Promise<string | null> {
|
|
||||||
// Only attempt if extraction service URL is configured
|
|
||||||
const extractionUrl = config.EXTRACTION_SERVICE_URL;
|
const extractionUrl = config.EXTRACTION_SERVICE_URL;
|
||||||
if (!extractionUrl) return null;
|
if (!extractionUrl) return null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const prompt = buildPrompt(input);
|
|
||||||
const res = await fetch(`${extractionUrl}/api/extract`, {
|
const res = await fetch(`${extractionUrl}/api/extract`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
@ -85,9 +73,7 @@ async function llmEnrich(input: ContextMessageInput): Promise<string | null> {
|
|||||||
}),
|
}),
|
||||||
signal: AbortSignal.timeout(5_000),
|
signal: AbortSignal.timeout(5_000),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!res.ok) return null;
|
if (!res.ok) return null;
|
||||||
|
|
||||||
const data = await res.json() as { result?: string };
|
const data = await res.json() as { result?: string };
|
||||||
return data.result ?? null;
|
return data.result ?? null;
|
||||||
} catch {
|
} catch {
|
||||||
@ -95,6 +81,37 @@ async function llmEnrich(input: ContextMessageInput): Promise<string | null> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function llmViaOllama(prompt: string): Promise<string | null> {
|
||||||
|
const ollamaUrl = config.OLLAMA_URL;
|
||||||
|
if (!ollamaUrl) return null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch(`${ollamaUrl}/api/generate`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
model: config.OLLAMA_MODEL,
|
||||||
|
prompt,
|
||||||
|
stream: false,
|
||||||
|
options: { num_predict: 80, temperature: 0.7 },
|
||||||
|
}),
|
||||||
|
signal: AbortSignal.timeout(5_000),
|
||||||
|
});
|
||||||
|
if (!res.ok) return null;
|
||||||
|
const data = await res.json() as { response?: string };
|
||||||
|
const text = data.response?.trim();
|
||||||
|
return text && text.length > 5 ? text : null;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function llmEnrich(input: ContextMessageInput): Promise<string | null> {
|
||||||
|
const prompt = buildPrompt(input);
|
||||||
|
// Try extraction-service first, then Ollama
|
||||||
|
return await llmViaExtraction(prompt) ?? await llmViaOllama(prompt);
|
||||||
|
}
|
||||||
|
|
||||||
function buildPrompt(input: ContextMessageInput): string {
|
function buildPrompt(input: ContextMessageInput): string {
|
||||||
const parts = [
|
const parts = [
|
||||||
`Timer: "${input.timerLabel}" fires in ${input.minutesBefore} minutes.`,
|
`Timer: "${input.timerLabel}" fires in ${input.minutesBefore} minutes.`,
|
||||||
|
|||||||
@ -15,6 +15,8 @@ const envSchema = baseBackendConfigSchema.extend({
|
|||||||
FIELD_ENCRYPT_MEK_NAME: z.string().default('chronomind-mek'),
|
FIELD_ENCRYPT_MEK_NAME: z.string().default('chronomind-mek'),
|
||||||
AZURE_KEYVAULT_URL: z.string().optional(),
|
AZURE_KEYVAULT_URL: z.string().optional(),
|
||||||
EXTRACTION_SERVICE_URL: z.string().optional(),
|
EXTRACTION_SERVICE_URL: z.string().optional(),
|
||||||
|
OLLAMA_URL: z.string().optional(),
|
||||||
|
OLLAMA_MODEL: z.string().default('gemma3:4b'),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const config = envSchema.parse(process.env);
|
export const config = envSchema.parse(process.env);
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user