mirror of
https://github.com/FJurmanovic/sugarr.git
synced 2026-11-11 07:41:08 +00:00
Group pipeline runs into Langfuse sessions, tagged by trigger source
Each pipeline invocation (runAsync/searchAsync) now gets a fresh sessionId, wrapped via the new Tracer.runWithSession() (implemented with @langfuse/tracing's propagateAttributes, since v5 has no direct sessionId/tags option on startObservation) so it shows up in Langfuse's Sessions view. The session is also tagged with what triggered the run: "cron", "api-list", "api-trigger", "api-search". Verified: full test suite (79/79) passing, tsc/eslint clean, real docker compose build + container smoke test against /health.
This commit is contained in:
@@ -40,7 +40,7 @@ export class RecommendationsController extends Controller {
|
||||
: this.config.LLM_ENABLED;
|
||||
|
||||
try {
|
||||
const recommendations = await this.pipeline.runAsync({ count, useLlm });
|
||||
const recommendations = await this.pipeline.runAsync({ count, useLlm }, { trigger: 'api-list' });
|
||||
reply.send(recommendations);
|
||||
} catch (err) {
|
||||
reply.code(500).send({ error: String(err) });
|
||||
@@ -49,10 +49,10 @@ export class RecommendationsController extends Controller {
|
||||
|
||||
private async handleTrigger(_request: FastifyRequest, reply: FastifyReply) {
|
||||
try {
|
||||
const recommendations = await this.pipeline.runAsync({
|
||||
count: this.config.RECOMMEND_COUNT,
|
||||
useLlm: this.config.LLM_ENABLED,
|
||||
});
|
||||
const recommendations = await this.pipeline.runAsync(
|
||||
{ count: this.config.RECOMMEND_COUNT, useLlm: this.config.LLM_ENABLED },
|
||||
{ trigger: 'api-trigger' }
|
||||
);
|
||||
if (this.config.RADARR_ENABLED) {
|
||||
await this.pipeline.autoRequest(recommendations);
|
||||
}
|
||||
@@ -73,7 +73,7 @@ export class RecommendationsController extends Controller {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const recommendations = await this.pipeline.searchAsync(query, this.config.RECOMMEND_COUNT);
|
||||
const recommendations = await this.pipeline.searchAsync(query, this.config.RECOMMEND_COUNT, 'api-search');
|
||||
reply.send(recommendations);
|
||||
} catch (err) {
|
||||
reply.code(500).send({ error: String(err) });
|
||||
|
||||
@@ -9,10 +9,10 @@ export function scheduleCron(config: AppConfig, pipeline: RecommendationPipeline
|
||||
cron.schedule(config.CRON_SCHEDULE, async () => {
|
||||
logger.info('Cron: running recommendation pipeline');
|
||||
try {
|
||||
const recommendations = await pipeline.runAsync({
|
||||
count: config.RECOMMEND_COUNT,
|
||||
useLlm: config.LLM_ENABLED,
|
||||
});
|
||||
const recommendations = await pipeline.runAsync(
|
||||
{ count: config.RECOMMEND_COUNT, useLlm: config.LLM_ENABLED },
|
||||
{ trigger: 'cron' }
|
||||
);
|
||||
if (config.RADARR_ENABLED) {
|
||||
await pipeline.autoRequest(recommendations);
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { randomUUID } from 'node:crypto';
|
||||
import { LlmPipeline } from '../../core/llm/LlmPipeline';
|
||||
import type { RunContext } from '../../core/llm/RunContext';
|
||||
import type { JellyfinClient } from '../clients/jellyfin/JellyfinClient';
|
||||
@@ -114,8 +115,16 @@ export class RecommendationPipeline extends LlmPipeline<RecommendationRequest, R
|
||||
}
|
||||
}
|
||||
|
||||
async searchAsync(query: string, count: number): Promise<Recommendation[]> {
|
||||
const trace = this.tracer.startTrace(`${this.name}-search`);
|
||||
async searchAsync(query: string, count: number, trigger?: string): Promise<Recommendation[]> {
|
||||
const sessionId = randomUUID();
|
||||
return this.tracer.runWithSession(
|
||||
{ sessionId, tags: trigger ? [trigger] : undefined },
|
||||
() => this.runSearch(query, count, trigger)
|
||||
);
|
||||
}
|
||||
|
||||
private async runSearch(query: string, count: number, trigger?: string): Promise<Recommendation[]> {
|
||||
const trace = this.tracer.startTrace(`${this.name}-search`, { trigger });
|
||||
const ctx: RunContext = { trace, logger: this.logger };
|
||||
try {
|
||||
let library: Movie[] = [];
|
||||
|
||||
@@ -1,9 +1,15 @@
|
||||
import { NodeSDK } from '@opentelemetry/sdk-node';
|
||||
import { LangfuseSpanProcessor } from '@langfuse/otel';
|
||||
import { startObservation } from '@langfuse/tracing';
|
||||
import { propagateAttributes, startObservation } from '@langfuse/tracing';
|
||||
import type { LangfuseGeneration, LangfuseSpan } from '@langfuse/tracing';
|
||||
import { Tracer } from '../llm/Tracer';
|
||||
import type { GenerationHandle, GenerationInput, GenerationOutput, TraceHandle } from '../llm/Tracer';
|
||||
import type {
|
||||
GenerationHandle,
|
||||
GenerationInput,
|
||||
GenerationOutput,
|
||||
SessionInfo,
|
||||
TraceHandle,
|
||||
} from '../llm/Tracer';
|
||||
|
||||
export class LangfuseTracer extends Tracer {
|
||||
private readonly processor: LangfuseSpanProcessor;
|
||||
@@ -48,6 +54,10 @@ export class LangfuseTracer extends Tracer {
|
||||
await this.processor.forceFlush();
|
||||
}
|
||||
|
||||
runWithSession<T>(session: SessionInfo, fn: () => Promise<T>): Promise<T> {
|
||||
return propagateAttributes({ sessionId: session.sessionId, tags: session.tags }, fn);
|
||||
}
|
||||
|
||||
async shutdown(): Promise<void> {
|
||||
await this.sdk.shutdown();
|
||||
}
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
import { randomUUID } from 'node:crypto';
|
||||
import type { Logger } from '../logging/logger';
|
||||
import type { LlmTool } from './LlmTool';
|
||||
import type { RunContext } from './RunContext';
|
||||
import type { Tracer } from './Tracer';
|
||||
|
||||
export interface PipelineRunOptions {
|
||||
/** What triggered this run (e.g. "cron", "api-list") — attached as a Langfuse tag. */
|
||||
trigger?: string;
|
||||
}
|
||||
|
||||
export abstract class LlmPipeline<TInput, TOutput> {
|
||||
constructor(
|
||||
protected readonly tracer: Tracer,
|
||||
@@ -12,14 +18,20 @@ export abstract class LlmPipeline<TInput, TOutput> {
|
||||
protected abstract readonly name: string;
|
||||
protected abstract runInternal(input: TInput, ctx: RunContext): Promise<TOutput>;
|
||||
|
||||
async runAsync(input: TInput): Promise<TOutput> {
|
||||
const trace = this.tracer.startTrace(this.name);
|
||||
const ctx: RunContext = { trace, logger: this.logger };
|
||||
try {
|
||||
return await this.runInternal(input, ctx);
|
||||
} finally {
|
||||
await this.tracer.flush();
|
||||
}
|
||||
async runAsync(input: TInput, options: PipelineRunOptions = {}): Promise<TOutput> {
|
||||
const sessionId = randomUUID();
|
||||
return this.tracer.runWithSession(
|
||||
{ sessionId, tags: options.trigger ? [options.trigger] : undefined },
|
||||
async () => {
|
||||
const trace = this.tracer.startTrace(this.name, { trigger: options.trigger });
|
||||
const ctx: RunContext = { trace, logger: this.logger };
|
||||
try {
|
||||
return await this.runInternal(input, ctx);
|
||||
} finally {
|
||||
await this.tracer.flush();
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
protected runTool<TToolInput, TToolOutput>(
|
||||
|
||||
@@ -20,7 +20,18 @@ export interface TraceHandle {
|
||||
startGeneration(input: GenerationInput): GenerationHandle;
|
||||
}
|
||||
|
||||
export interface SessionInfo {
|
||||
sessionId: string;
|
||||
tags?: string[];
|
||||
}
|
||||
|
||||
export abstract class Tracer {
|
||||
abstract startTrace(name: string, metadata?: Record<string, unknown>): TraceHandle;
|
||||
abstract flush(): Promise<void>;
|
||||
|
||||
/**
|
||||
* Runs `fn` with `session` (sessionId/tags) attached to every trace/span
|
||||
* created inside it, so multiple runs can be grouped in the Langfuse UI.
|
||||
*/
|
||||
abstract runWithSession<T>(session: SessionInfo, fn: () => Promise<T>): Promise<T>;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user