mirror of
https://github.com/FJurmanovic/sugarr.git
synced 2026-11-11 07:41:08 +00:00
- ApiClient: strip query string from ApiError messages so API keys (e.g. TMDb's api_key query param) never leak into 500 response bodies or logs. - LlmTool: accept an optional defaultModel, wired from AppConfig's LLM_MODEL, so Langfuse prompts that omit a model config fall back to the configured LLM rather than silently requesting gpt-4o-mini. - RecommendationPipeline: restore the Go original's graceful degradation — library/Radarr fetch failures, per-suggestion TMDb resolution failures, and per-signal TMDb similarity/recommendation failures are now logged and skipped instead of aborting the whole run (applies to both runInternal and searchAsync). - RecommendationsController: validate the ?count= query param (must be a positive integer) instead of silently producing NaN. - Commit the Langfuse prompt definitions (langfuse/*.prompt.json) and add a README covering setup, prompt seeding, cache invalidation, and the HTTP API surface — the branch was not otherwise runnable by anyone but the author.
61 lines
2.2 KiB
TypeScript
61 lines
2.2 KiB
TypeScript
import type { PromptService } from '../langfuse/PromptService';
|
|
import type { ChatMessage, LlmClient } from './LlmClient';
|
|
import type { RunContext } from './RunContext';
|
|
|
|
export abstract class LlmTool<TInput, TOutput> {
|
|
protected abstract readonly promptName: string;
|
|
|
|
constructor(
|
|
protected readonly promptService: PromptService,
|
|
protected readonly llmClient: LlmClient,
|
|
protected readonly defaultModel: string = 'gpt-4o-mini'
|
|
) {}
|
|
|
|
protected abstract buildVariables(input: TInput, ctx: RunContext): Record<string, unknown>;
|
|
|
|
protected async preProcess(input: TInput, _ctx: RunContext): Promise<TInput> {
|
|
return input;
|
|
}
|
|
|
|
protected async postProcess(output: TOutput, _input: TInput, _ctx: RunContext): Promise<TOutput> {
|
|
return output;
|
|
}
|
|
|
|
protected abstract parseOutput(raw: string, ctx: RunContext): TOutput;
|
|
|
|
async runAsync(input: TInput, ctx: RunContext): Promise<TOutput> {
|
|
const processedInput = await this.preProcess(input, ctx);
|
|
const variables = this.buildVariables(processedInput, ctx);
|
|
const compiled = await this.promptService.getCompiled(this.promptName, variables);
|
|
|
|
const model = String(compiled.config.model ?? this.defaultModel);
|
|
const temperature =
|
|
typeof compiled.config.temperature === 'number' ? compiled.config.temperature : undefined;
|
|
const maxTokens =
|
|
typeof compiled.config.maxTokens === 'number' ? compiled.config.maxTokens : undefined;
|
|
|
|
const messages: ChatMessage[] = compiled.messages;
|
|
const generation = ctx.trace.startGeneration({
|
|
name: this.promptName,
|
|
model,
|
|
input: messages,
|
|
metadata: { promptVersion: compiled.version },
|
|
});
|
|
|
|
let raw: string;
|
|
try {
|
|
const result = await this.llmClient.chat(messages, { model, temperature, maxTokens });
|
|
raw = result.content;
|
|
generation.end({ output: raw, usage: result.usage });
|
|
} catch (err) {
|
|
generation.end({ output: null, metadata: { error: String(err) } });
|
|
throw err;
|
|
}
|
|
|
|
const parsed = this.parseOutput(raw, ctx);
|
|
const output = await this.postProcess(parsed, processedInput, ctx);
|
|
ctx.logger.info({ tool: this.promptName }, 'llm tool completed');
|
|
return output;
|
|
}
|
|
}
|