gavio 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/gateway.js +46 -0
- package/dist/cjs/index.js +4 -2
- package/dist/cjs/interceptors/audit/interceptor.js +4 -0
- package/dist/cjs/interceptors/audit/record.js +17 -3
- package/dist/cjs/interceptors/metrics/index.js +9 -0
- package/dist/cjs/interceptors/metrics/interceptor.js +37 -0
- package/dist/cjs/interceptors/metrics/registry.js +0 -0
- package/dist/cjs/interceptors/quality/index.js +7 -0
- package/dist/cjs/interceptors/quality/risk.js +49 -0
- package/dist/cjs/interceptors/reliability/index.js +3 -1
- package/dist/cjs/interceptors/reliability/stream-buffer.js +28 -0
- package/dist/cjs/providers/base.js +9 -0
- package/dist/cjs/request.js +3 -0
- package/dist/cjs/types.js +53 -1
- package/dist/esm/gateway.d.ts +13 -1
- package/dist/esm/gateway.js +46 -0
- package/dist/esm/index.d.ts +3 -3
- package/dist/esm/index.js +2 -2
- package/dist/esm/interceptors/audit/interceptor.js +4 -0
- package/dist/esm/interceptors/audit/record.d.ts +4 -2
- package/dist/esm/interceptors/audit/record.js +18 -4
- package/dist/esm/interceptors/metrics/index.d.ts +5 -0
- package/dist/esm/interceptors/metrics/index.js +3 -0
- package/dist/esm/interceptors/metrics/interceptor.d.ts +22 -0
- package/dist/esm/interceptors/metrics/interceptor.js +33 -0
- package/dist/esm/interceptors/metrics/registry.d.ts +31 -0
- package/dist/esm/interceptors/metrics/registry.js +0 -0
- package/dist/esm/interceptors/quality/index.d.ts +3 -0
- package/dist/esm/interceptors/quality/index.js +2 -0
- package/dist/esm/interceptors/quality/risk.d.ts +32 -0
- package/dist/esm/interceptors/quality/risk.js +44 -0
- package/dist/esm/interceptors/reliability/index.d.ts +1 -0
- package/dist/esm/interceptors/reliability/index.js +1 -0
- package/dist/esm/interceptors/reliability/stream-buffer.d.ts +18 -0
- package/dist/esm/interceptors/reliability/stream-buffer.js +24 -0
- package/dist/esm/providers/base.d.ts +7 -0
- package/dist/esm/providers/base.js +9 -1
- package/dist/esm/request.d.ts +4 -1
- package/dist/esm/request.js +4 -1
- package/dist/esm/types.d.ts +54 -0
- package/dist/esm/types.js +50 -0
- package/package.json +11 -1
- package/src/gateway.ts +52 -1
- package/src/index.ts +4 -2
- package/src/interceptors/audit/interceptor.ts +4 -0
- package/src/interceptors/audit/record.ts +18 -4
- package/src/interceptors/metrics/index.ts +6 -0
- package/src/interceptors/metrics/interceptor.ts +46 -0
- package/src/interceptors/metrics/registry.ts +0 -0
- package/src/interceptors/quality/index.ts +4 -0
- package/src/interceptors/quality/risk.ts +64 -0
- package/src/interceptors/reliability/index.ts +1 -0
- package/src/interceptors/reliability/stream-buffer.ts +27 -0
- package/src/providers/base.ts +21 -1
- package/src/request.ts +6 -2
- package/src/types.ts +77 -0
|
@@ -3,12 +3,14 @@
|
|
|
3
3
|
import type { InterceptorContext } from '../../context.js'
|
|
4
4
|
import type { GavioRequest } from '../../request.js'
|
|
5
5
|
import type { GavioResponse } from '../../response.js'
|
|
6
|
+
import type { PromptLineage } from '../../types.js'
|
|
6
7
|
import type { Interceptor } from '../base.js'
|
|
7
8
|
import { AuditRecord } from './record.js'
|
|
8
9
|
import type { AuditSink } from './sink.js'
|
|
9
10
|
import { stdoutSink } from './sinks/stdout.js'
|
|
10
11
|
|
|
11
12
|
const PROMPT_HASH_KEY = 'audit_prompt_hash'
|
|
13
|
+
const LINEAGE_KEY = 'audit_lineage'
|
|
12
14
|
|
|
13
15
|
export const AUDIT_NAME = 'audit'
|
|
14
16
|
|
|
@@ -41,6 +43,7 @@ class AuditInterceptor implements Interceptor {
|
|
|
41
43
|
|
|
42
44
|
async before(request: GavioRequest, ctx: InterceptorContext): Promise<GavioRequest> {
|
|
43
45
|
ctx.state[PROMPT_HASH_KEY] = AuditRecord.hashText(request.promptText())
|
|
46
|
+
if (request.lineage != null) ctx.state[LINEAGE_KEY] = request.lineage
|
|
44
47
|
return request
|
|
45
48
|
}
|
|
46
49
|
|
|
@@ -69,6 +72,7 @@ class AuditInterceptor implements Interceptor {
|
|
|
69
72
|
cacheType: response.cacheType,
|
|
70
73
|
guardrailOutcome: ctx.guardrailOutcome,
|
|
71
74
|
riskScore: ctx.riskScore,
|
|
75
|
+
lineage: (ctx.state[LINEAGE_KEY] as PromptLineage | undefined) ?? null,
|
|
72
76
|
})
|
|
73
77
|
if (this.hashChain) {
|
|
74
78
|
record.previousHash = this.lastHash
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/** AuditRecord — the immutable, per-request audit entry. */
|
|
2
2
|
|
|
3
3
|
import { createHash } from 'node:crypto'
|
|
4
|
-
import { TokenUsage } from '../../types.js'
|
|
4
|
+
import { PromptLineage, TokenUsage } from '../../types.js'
|
|
5
5
|
|
|
6
6
|
export const SCHEMA_VERSION = '1.0'
|
|
7
7
|
|
|
@@ -9,6 +9,17 @@ function sha256(text: string): string {
|
|
|
9
9
|
return createHash('sha256').update(text, 'utf-8').digest('hex')
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
+
/** Deterministic JSON with keys sorted at every nesting level. */
|
|
13
|
+
function stableStringify(value: unknown): string {
|
|
14
|
+
if (value === null || typeof value !== 'object') return JSON.stringify(value) ?? 'null'
|
|
15
|
+
if (Array.isArray(value)) return `[${value.map(stableStringify).join(',')}]`
|
|
16
|
+
const obj = value as Record<string, unknown>
|
|
17
|
+
const parts = Object.keys(obj)
|
|
18
|
+
.sort()
|
|
19
|
+
.map((k) => `${JSON.stringify(k)}:${stableStringify(obj[k])}`)
|
|
20
|
+
return `{${parts.join(',')}}`
|
|
21
|
+
}
|
|
22
|
+
|
|
12
23
|
export interface AuditRecordInit {
|
|
13
24
|
traceId: string
|
|
14
25
|
provider: string
|
|
@@ -30,6 +41,7 @@ export interface AuditRecordInit {
|
|
|
30
41
|
cacheType?: string | null
|
|
31
42
|
guardrailOutcome?: string | null
|
|
32
43
|
riskScore?: number | null
|
|
44
|
+
lineage?: PromptLineage | null
|
|
33
45
|
previousHash?: string
|
|
34
46
|
schemaVersion?: string
|
|
35
47
|
}
|
|
@@ -62,6 +74,7 @@ export class AuditRecord {
|
|
|
62
74
|
cacheType: string | null
|
|
63
75
|
guardrailOutcome: string | null
|
|
64
76
|
riskScore: number | null
|
|
77
|
+
lineage: PromptLineage | null
|
|
65
78
|
previousHash: string
|
|
66
79
|
schemaVersion: string
|
|
67
80
|
|
|
@@ -86,6 +99,7 @@ export class AuditRecord {
|
|
|
86
99
|
this.cacheType = init.cacheType ?? null
|
|
87
100
|
this.guardrailOutcome = init.guardrailOutcome ?? null
|
|
88
101
|
this.riskScore = init.riskScore ?? null
|
|
102
|
+
this.lineage = init.lineage ?? null
|
|
89
103
|
this.previousHash = init.previousHash ?? ''
|
|
90
104
|
this.schemaVersion = init.schemaVersion ?? SCHEMA_VERSION
|
|
91
105
|
}
|
|
@@ -120,15 +134,15 @@ export class AuditRecord {
|
|
|
120
134
|
cacheType: this.cacheType,
|
|
121
135
|
guardrailOutcome: this.guardrailOutcome,
|
|
122
136
|
riskScore: this.riskScore,
|
|
137
|
+
lineage: this.lineage ? this.lineage.toJSON() : null,
|
|
123
138
|
previousHash: this.previousHash,
|
|
124
139
|
schemaVersion: this.schemaVersion,
|
|
125
140
|
}
|
|
126
141
|
}
|
|
127
142
|
|
|
128
|
-
/** Stable JSON with sorted keys — used for the v0.2.0 hash chain. */
|
|
143
|
+
/** Stable JSON with recursively sorted keys — used for the v0.2.0 hash chain. */
|
|
129
144
|
toCanonicalJson(): string {
|
|
130
|
-
|
|
131
|
-
return JSON.stringify(data, Object.keys(data).sort())
|
|
145
|
+
return stableStringify(this.toJSON())
|
|
132
146
|
}
|
|
133
147
|
|
|
134
148
|
/** Hash of this record's content — used to build the v0.2.0 chain. */
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/** Prometheus metrics (F-OBS-08). */
|
|
2
|
+
|
|
3
|
+
export { PrometheusMetrics } from './registry.js'
|
|
4
|
+
export type { RecordSample } from './registry.js'
|
|
5
|
+
export { metricsInterceptor, METRICS_NAME } from './interceptor.js'
|
|
6
|
+
export type { MetricsInterceptor } from './interceptor.js'
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/** metricsInterceptor (F-OBS-08) — records Prometheus metrics per request. */
|
|
2
|
+
|
|
3
|
+
import type { InterceptorContext } from '../../context.js'
|
|
4
|
+
import type { GavioResponse } from '../../response.js'
|
|
5
|
+
import type { Interceptor } from '../base.js'
|
|
6
|
+
import { PrometheusMetrics } from './registry.js'
|
|
7
|
+
|
|
8
|
+
export const METRICS_NAME = 'metrics'
|
|
9
|
+
|
|
10
|
+
/** An interceptor that also exposes the registry it records into. */
|
|
11
|
+
export interface MetricsInterceptor extends Interceptor {
|
|
12
|
+
readonly metrics: PrometheusMetrics
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Build a metrics interceptor. Pass a shared {@link PrometheusMetrics} registry
|
|
17
|
+
* (or let it create one) and scrape it via `.metrics.render()`:
|
|
18
|
+
*
|
|
19
|
+
* ```ts
|
|
20
|
+
* const m = metricsInterceptor()
|
|
21
|
+
* const gw = new Gateway({ devMode: true }).use(m)
|
|
22
|
+
* // ...
|
|
23
|
+
* console.log(m.metrics.render())
|
|
24
|
+
* ```
|
|
25
|
+
*
|
|
26
|
+
* Observation-only, so it always runs (including in dry-run).
|
|
27
|
+
*/
|
|
28
|
+
export function metricsInterceptor(
|
|
29
|
+
metrics: PrometheusMetrics = new PrometheusMetrics(),
|
|
30
|
+
): MetricsInterceptor {
|
|
31
|
+
return {
|
|
32
|
+
name: METRICS_NAME,
|
|
33
|
+
dryRunSafe: true,
|
|
34
|
+
metrics,
|
|
35
|
+
async after(response: GavioResponse, _ctx: InterceptorContext): Promise<GavioResponse> {
|
|
36
|
+
metrics.record(response.provider, response.model, {
|
|
37
|
+
promptTokens: response.usage.promptTokens,
|
|
38
|
+
completionTokens: response.usage.completionTokens,
|
|
39
|
+
costUsd: response.costUsd,
|
|
40
|
+
latencyMs: response.latencyMs,
|
|
41
|
+
cacheHit: response.cacheHit,
|
|
42
|
+
})
|
|
43
|
+
return response
|
|
44
|
+
},
|
|
45
|
+
}
|
|
46
|
+
}
|
|
Binary file
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RiskScorer (F-QUA-06) — a composite risk score from per-request signals.
|
|
3
|
+
*
|
|
4
|
+
* Folds the signals other interceptors leave on the {@link InterceptorContext}
|
|
5
|
+
* — PII entities found, guardrail outcome, and the prompt-injection risk — into
|
|
6
|
+
* a single score in `[0, 1]` written to `ctx.riskScore` (and thus the audit
|
|
7
|
+
* record). Register it *inside* the audit interceptor so audit sees the composite.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { InterceptorContext } from '../../context.js'
|
|
11
|
+
import type { GavioResponse } from '../../response.js'
|
|
12
|
+
import type { Interceptor } from '../base.js'
|
|
13
|
+
|
|
14
|
+
export interface RiskWeights {
|
|
15
|
+
pii?: number
|
|
16
|
+
guardrail?: number
|
|
17
|
+
injection?: number
|
|
18
|
+
/** PII entity count at which the PII signal saturates to 1.0 (<= 0 → any PII = 1.0). */
|
|
19
|
+
piiSaturation?: number
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Guardrail outcome → its contribution before weighting.
|
|
23
|
+
const GUARDRAIL_SIGNAL: Record<string, number> = { FAIL: 1.0, HITL: 0.6 }
|
|
24
|
+
|
|
25
|
+
export class RiskScorer implements Interceptor {
|
|
26
|
+
readonly name = 'risk_scorer'
|
|
27
|
+
readonly dryRunSafe = true
|
|
28
|
+
|
|
29
|
+
private readonly pii: number
|
|
30
|
+
private readonly guardrail: number
|
|
31
|
+
private readonly injection: number
|
|
32
|
+
private readonly piiSaturation: number
|
|
33
|
+
|
|
34
|
+
constructor(weights: RiskWeights = {}) {
|
|
35
|
+
this.pii = weights.pii ?? 0.3
|
|
36
|
+
this.guardrail = weights.guardrail ?? 0.4
|
|
37
|
+
this.injection = weights.injection ?? 0.3
|
|
38
|
+
this.piiSaturation = weights.piiSaturation ?? 4
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Compute the composite risk score from the three raw signals. */
|
|
42
|
+
score(piiCount: number, guardrailOutcome: string | null, injectionScore: number | null): number {
|
|
43
|
+
let piiSignal = 0
|
|
44
|
+
if (piiCount > 0) {
|
|
45
|
+
piiSignal = this.piiSaturation <= 0 ? 1 : Math.min(1, piiCount / this.piiSaturation)
|
|
46
|
+
}
|
|
47
|
+
const guardrailSignal = GUARDRAIL_SIGNAL[guardrailOutcome ?? ''] ?? 0
|
|
48
|
+
const injectionSignal = injectionScore ?? 0
|
|
49
|
+
const composite =
|
|
50
|
+
this.pii * piiSignal + this.guardrail * guardrailSignal + this.injection * injectionSignal
|
|
51
|
+
return Math.max(0, Math.min(1, composite))
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async after(response: GavioResponse, ctx: InterceptorContext): Promise<GavioResponse> {
|
|
55
|
+
const piiCount = Object.values(ctx.piiEntityCounts).reduce((a, b) => a + b, 0)
|
|
56
|
+
ctx.riskScore = this.score(piiCount, ctx.guardrailOutcome, ctx.riskScore)
|
|
57
|
+
return response
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Build a risk scorer. */
|
|
62
|
+
export function riskScorer(weights: RiskWeights = {}): RiskScorer {
|
|
63
|
+
return new RiskScorer(weights)
|
|
64
|
+
}
|
|
@@ -10,3 +10,4 @@ export { circuitBreaker, CircuitState } from './circuit-breaker.js'
|
|
|
10
10
|
export type { CircuitBreakerOptions } from './circuit-breaker.js'
|
|
11
11
|
export { loadBalancer } from './load-balancer.js'
|
|
12
12
|
export type { LoadBalancerOptions } from './load-balancer.js'
|
|
13
|
+
export { StreamBuffer } from './stream-buffer.js'
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* StreamBuffer (F-REL-06) — accumulate a provider stream for post-interceptors.
|
|
3
|
+
*
|
|
4
|
+
* Post-interceptors (guardrails, PII restore, audit) need the *complete*
|
|
5
|
+
* response, so a streamed reply is buffered in full before the post pipeline
|
|
6
|
+
* runs and before any chunk reaches the caller. This trades first-token latency
|
|
7
|
+
* for the guarantee that every interceptor sees — and can rewrite or block — the
|
|
8
|
+
* whole response.
|
|
9
|
+
*/
|
|
10
|
+
export class StreamBuffer {
|
|
11
|
+
private readonly parts: string[] = []
|
|
12
|
+
|
|
13
|
+
/** Add one streamed chunk. */
|
|
14
|
+
append(chunk: string): void {
|
|
15
|
+
this.parts.push(chunk)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** The full buffered response so far. */
|
|
19
|
+
text(): string {
|
|
20
|
+
return this.parts.join('')
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Total buffered length in characters. */
|
|
24
|
+
get length(): number {
|
|
25
|
+
return this.parts.reduce((n, p) => n + p.length, 0)
|
|
26
|
+
}
|
|
27
|
+
}
|
package/src/providers/base.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/** ProviderAdapter interface and shared response-building helpers. */
|
|
2
2
|
|
|
3
|
-
import { PricingProvider } from '../pricing.js'
|
|
3
|
+
import { PricingProvider, estimateTokens } from '../pricing.js'
|
|
4
4
|
import { GavioRequest } from '../request.js'
|
|
5
5
|
import { GavioResponse } from '../response.js'
|
|
6
6
|
import { TokenUsage } from '../types.js'
|
|
@@ -10,6 +10,8 @@ export interface ProviderAdapter {
|
|
|
10
10
|
readonly providerName: string
|
|
11
11
|
complete(request: GavioRequest): Promise<GavioResponse>
|
|
12
12
|
stream?(request: GavioRequest): AsyncIterable<string>
|
|
13
|
+
/** Build a response from a fully buffered stream (F-REL-06). */
|
|
14
|
+
buildStreamResponse?(request: GavioRequest, content: string, startedAt: number): GavioResponse
|
|
13
15
|
healthCheck(): Promise<boolean>
|
|
14
16
|
readonly reportedModelVersion?: string | null
|
|
15
17
|
}
|
|
@@ -30,6 +32,24 @@ export abstract class BaseProviderAdapter implements ProviderAdapter {
|
|
|
30
32
|
return null
|
|
31
33
|
}
|
|
32
34
|
|
|
35
|
+
/**
|
|
36
|
+
* Build a response from a fully buffered stream (F-REL-06). Streamed chunks
|
|
37
|
+
* carry text only, so token usage is estimated from prompt + content.
|
|
38
|
+
*/
|
|
39
|
+
buildStreamResponse(request: GavioRequest, content: string, startedAt: number): GavioResponse {
|
|
40
|
+
const usage = new TokenUsage(
|
|
41
|
+
estimateTokens(request.promptText()),
|
|
42
|
+
estimateTokens(content),
|
|
43
|
+
)
|
|
44
|
+
return this.buildResponse(
|
|
45
|
+
request,
|
|
46
|
+
content,
|
|
47
|
+
usage,
|
|
48
|
+
this.reportedModelVersion ?? request.model,
|
|
49
|
+
startedAt,
|
|
50
|
+
)
|
|
51
|
+
}
|
|
52
|
+
|
|
33
53
|
protected buildResponse(
|
|
34
54
|
request: GavioRequest,
|
|
35
55
|
content: string,
|
package/src/request.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/** GavioRequest — the canonical, provider-agnostic request model. */
|
|
2
2
|
|
|
3
3
|
import { newTraceId } from './ids.js'
|
|
4
|
-
import { coerceProvider } from './types.js'
|
|
5
|
-
import type { Message, Provider } from './types.js'
|
|
4
|
+
import { coerceProvider, PromptLineage } from './types.js'
|
|
5
|
+
import type { Message, PromptLineageInit, Provider } from './types.js'
|
|
6
6
|
|
|
7
7
|
export interface GavioRequestInit {
|
|
8
8
|
messages: Message[]
|
|
@@ -14,6 +14,7 @@ export interface GavioRequestInit {
|
|
|
14
14
|
sessionId?: string | null
|
|
15
15
|
options?: Record<string, unknown>
|
|
16
16
|
metadata?: Record<string, unknown>
|
|
17
|
+
lineage?: PromptLineage | PromptLineageInit | null
|
|
17
18
|
}
|
|
18
19
|
|
|
19
20
|
/**
|
|
@@ -31,6 +32,7 @@ export class GavioRequest {
|
|
|
31
32
|
sessionId: string | null
|
|
32
33
|
options: Record<string, unknown>
|
|
33
34
|
metadata: Record<string, unknown>
|
|
35
|
+
lineage: PromptLineage | null
|
|
34
36
|
|
|
35
37
|
constructor(init: GavioRequestInit) {
|
|
36
38
|
this.messages = init.messages
|
|
@@ -42,6 +44,7 @@ export class GavioRequest {
|
|
|
42
44
|
this.sessionId = init.sessionId ?? null
|
|
43
45
|
this.options = init.options ?? {}
|
|
44
46
|
this.metadata = init.metadata ?? {}
|
|
47
|
+
this.lineage = init.lineage != null ? PromptLineage.from(init.lineage) : null
|
|
45
48
|
}
|
|
46
49
|
|
|
47
50
|
get temperature(): number {
|
|
@@ -71,6 +74,7 @@ export class GavioRequest {
|
|
|
71
74
|
sessionId: this.sessionId,
|
|
72
75
|
options: { ...this.options },
|
|
73
76
|
metadata: { ...this.metadata },
|
|
77
|
+
lineage: this.lineage,
|
|
74
78
|
})
|
|
75
79
|
}
|
|
76
80
|
}
|
package/src/types.ts
CHANGED
|
@@ -81,3 +81,80 @@ export class TokenUsage {
|
|
|
81
81
|
}
|
|
82
82
|
}
|
|
83
83
|
}
|
|
84
|
+
|
|
85
|
+
export interface RagChunkInit {
|
|
86
|
+
source: string
|
|
87
|
+
chunkId?: string | null
|
|
88
|
+
score?: number | null
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* A single retrieved source that contributed to a prompt. Carries a *reference*
|
|
93
|
+
* to the source — never the retrieved text — so prompt lineage stays within the
|
|
94
|
+
* audit record's metadata-only contract.
|
|
95
|
+
*/
|
|
96
|
+
export class RagChunk {
|
|
97
|
+
readonly source: string
|
|
98
|
+
readonly chunkId: string | null
|
|
99
|
+
readonly score: number | null
|
|
100
|
+
|
|
101
|
+
constructor(init: RagChunkInit) {
|
|
102
|
+
this.source = init.source
|
|
103
|
+
this.chunkId = init.chunkId ?? null
|
|
104
|
+
this.score = init.score ?? null
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
toJSON(): { source: string; chunkId: string | null; score: number | null } {
|
|
108
|
+
return { source: this.source, chunkId: this.chunkId, score: this.score }
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export interface PromptLineageInit {
|
|
113
|
+
templateId?: string | null
|
|
114
|
+
templateVersion?: string | null
|
|
115
|
+
variables?: Record<string, unknown>
|
|
116
|
+
ragChunks?: Array<RagChunk | RagChunkInit>
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Provenance for a rendered prompt (F-OBS-04): the template, the variable
|
|
121
|
+
* bindings interpolated into it, and the RAG chunk sources retrieved for it.
|
|
122
|
+
*
|
|
123
|
+
* Attached to a GavioRequest by the caller and copied into the AuditRecord so
|
|
124
|
+
* any prompt can be reconstructed and debugged. RAG chunk text is never stored
|
|
125
|
+
* — only source references (see {@link RagChunk}).
|
|
126
|
+
*/
|
|
127
|
+
export class PromptLineage {
|
|
128
|
+
readonly templateId: string | null
|
|
129
|
+
readonly templateVersion: string | null
|
|
130
|
+
readonly variables: Record<string, unknown>
|
|
131
|
+
readonly ragChunks: RagChunk[]
|
|
132
|
+
|
|
133
|
+
constructor(init: PromptLineageInit = {}) {
|
|
134
|
+
this.templateId = init.templateId ?? null
|
|
135
|
+
this.templateVersion = init.templateVersion ?? null
|
|
136
|
+
this.variables = init.variables ?? {}
|
|
137
|
+
this.ragChunks = (init.ragChunks ?? []).map((c) =>
|
|
138
|
+
c instanceof RagChunk ? c : new RagChunk(c),
|
|
139
|
+
)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** Coerce a PromptLineage instance or plain init object into a PromptLineage. */
|
|
143
|
+
static from(value: PromptLineage | PromptLineageInit): PromptLineage {
|
|
144
|
+
return value instanceof PromptLineage ? value : new PromptLineage(value)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
toJSON(): {
|
|
148
|
+
templateId: string | null
|
|
149
|
+
templateVersion: string | null
|
|
150
|
+
variables: Record<string, unknown>
|
|
151
|
+
ragChunks: Array<{ source: string; chunkId: string | null; score: number | null }>
|
|
152
|
+
} {
|
|
153
|
+
return {
|
|
154
|
+
templateId: this.templateId,
|
|
155
|
+
templateVersion: this.templateVersion,
|
|
156
|
+
variables: this.variables,
|
|
157
|
+
ragChunks: this.ragChunks.map((c) => c.toJSON()),
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|