776 lines
33 KiB
TypeScript
776 lines
33 KiB
TypeScript
// SPDX-FileCopyrightText: 2025-2026 Lucio Lelii <lucio.lelii@isti.cnr.it> - ISTI-CNR
|
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
// Attribution term under AGPL-3.0 section 7(b): see LICENSE-ADDENDUM.
|
|
|
|
import { HttpClient, HttpParams } from '@angular/common/http';
|
|
import { inject } from '@angular/core';
|
|
import { environment } from '@environment';
|
|
import { FlowBlockConnection, FlowData, FlowNodeDependency, LLMDescriptor } from '@models/flow';
|
|
import {
|
|
BiasDownstreamImpactEntry,
|
|
BiasImpactExperimentRequest,
|
|
BiasImpactJob,
|
|
BiasImpactJobStatus,
|
|
BiasImpactReport,
|
|
BiasImpactReportKind,
|
|
BiasItemImpact,
|
|
BiasIterationImpact,
|
|
BiasJudgeAttribution,
|
|
BiasJudgeImpactLevel,
|
|
BiasJudgeSummary,
|
|
BiasJudgeVerdict,
|
|
BiasMockedSideEffect,
|
|
BiasNumericDelta,
|
|
BiasOutcomeChangeEntry,
|
|
BiasRerunRequest,
|
|
BiasRoutingChangeEntry,
|
|
BiasSimulationContext,
|
|
BiasValueImpact
|
|
} from '@models/bias-impact';
|
|
import { ExecutionEventLogEntry, TaskExecution, TaskExecutionGroup, normalizeExecutionOutcomes } from '@models/task-execution';
|
|
import { map, Observable } from 'rxjs';
|
|
import { TaskExecutionsCallServiceBase } from './task-executions-call.base';
|
|
|
|
export class TaskExecutionsCallService extends TaskExecutionsCallServiceBase {
|
|
private readonly http = inject(HttpClient);
|
|
|
|
override retrieveAllTaskExecutions(): Observable<TaskExecution[]> {
|
|
return this.http.get<unknown[]>(`${environment.apiUrl}/executions`).pipe(
|
|
map((raw) => Array.isArray(raw) ? raw.map((item) => this.mapExecution(item)) : [])
|
|
);
|
|
}
|
|
|
|
override retrieveTaskExecutionGroups(): Observable<TaskExecutionGroup[]> {
|
|
return this.http.get<unknown>(`${environment.apiUrl}/executions/groups`).pipe(
|
|
map((raw) => Array.isArray(raw) ? raw.map((item) => this.mapExecutionGroup(item)) : [])
|
|
);
|
|
}
|
|
|
|
override retrieveTaskExecution(executionId: string): Observable<TaskExecution> {
|
|
return this.http
|
|
.get<unknown>(`${environment.apiUrl}/executions/${encodeURIComponent(executionId)}`)
|
|
.pipe(map((raw) => this.mapExecution(raw)));
|
|
}
|
|
|
|
override retrieveStepIterations(executionId: string, stepId: string): Observable<TaskExecution[]> {
|
|
const url = `${environment.apiUrl}/executions/${encodeURIComponent(executionId)}/node/${encodeURIComponent(stepId)}/iterations`;
|
|
return this.http.get<unknown>(url).pipe(
|
|
map((raw) => Array.isArray(raw) ? raw.map((item) => this.mapExecution(item)) : [])
|
|
);
|
|
}
|
|
|
|
override retrieveExecutionEvents(executionId: string): Observable<ExecutionEventLogEntry[]> {
|
|
return this.http.get<ExecutionEventLogEntry[]>(`${environment.apiUrl}/executions/${encodeURIComponent(executionId)}/events`);
|
|
}
|
|
|
|
override createTaskExecution(flowId: string): Observable<TaskExecution> {
|
|
return this.http.post<unknown>(`${environment.apiUrl}/executions`, flowId).pipe(
|
|
map((raw) => this.mapExecution(raw))
|
|
);
|
|
}
|
|
|
|
override rerunTaskExecution(executionId: string): Observable<TaskExecution> {
|
|
return this.http.post<unknown>(`${environment.apiUrl}/executions/${encodeURIComponent(executionId)}/rerun`, null).pipe(
|
|
map((raw) => this.mapExecution(raw))
|
|
);
|
|
}
|
|
|
|
override runBiasImpactExperiment(
|
|
executionId: string,
|
|
stepId: string,
|
|
request: BiasImpactExperimentRequest
|
|
): Observable<BiasImpactJob> {
|
|
const url = `${environment.apiUrl}/executions/${encodeURIComponent(executionId)}/steps/${encodeURIComponent(stepId)}/bias-impact`;
|
|
return this.http.post<unknown>(url, request).pipe(map((raw) => this.biasImpactJobFromApi(raw)));
|
|
}
|
|
|
|
override judgeBiasImpactReport(reportId: string, judge: LLMDescriptor, credentialId?: string): Observable<BiasImpactJob> {
|
|
const url = `${environment.apiUrl}/executions/bias-impact-reports/${encodeURIComponent(reportId)}/judge`;
|
|
return this.http.post<unknown>(url, { judge, credentialId }).pipe(map((raw) => this.biasImpactJobFromApi(raw)));
|
|
}
|
|
|
|
override getBiasImpactJob(jobId: string): Observable<BiasImpactJob> {
|
|
return this.http
|
|
.get<unknown>(`${environment.apiUrl}/executions/bias-impact-jobs/${encodeURIComponent(jobId)}`)
|
|
.pipe(map((raw) => this.biasImpactJobFromApi(raw)));
|
|
}
|
|
|
|
override createBiasedRerun(executionId: string, request: BiasRerunRequest): Observable<TaskExecution> {
|
|
return this.http
|
|
.post<unknown>(`${environment.apiUrl}/executions/${encodeURIComponent(executionId)}/bias-rerun`, request)
|
|
.pipe(map((raw) => this.mapExecution(raw)));
|
|
}
|
|
|
|
override compareBiasExecutions(
|
|
baselineExecutionId: string,
|
|
biasedExecutionId: string,
|
|
includeRawOutputs: boolean
|
|
): Observable<BiasImpactReport> {
|
|
const url = `${environment.apiUrl}/executions/${encodeURIComponent(baselineExecutionId)}/bias-compare/${encodeURIComponent(biasedExecutionId)}`;
|
|
return this.http.post<unknown>(url, null, {
|
|
params: new HttpParams().set('includeRawOutputs', String(includeRawOutputs))
|
|
}).pipe(map((raw) => this.biasImpactReportFromApi(raw)));
|
|
}
|
|
|
|
override listBiasImpactReports(executionId: string): Observable<BiasImpactReport[]> {
|
|
return this.http
|
|
.get<unknown>(`${environment.apiUrl}/executions/${encodeURIComponent(executionId)}/bias-impact-reports`)
|
|
.pipe(map((raw) => Array.isArray(raw) ? raw.map((item) => this.biasImpactReportFromApi(item)) : []));
|
|
}
|
|
|
|
override getBiasImpactReport(reportId: string): Observable<BiasImpactReport> {
|
|
return this.http
|
|
.get<unknown>(`${environment.apiUrl}/executions/bias-impact-reports/${encodeURIComponent(reportId)}`)
|
|
.pipe(map((raw) => this.biasImpactReportFromApi(raw)));
|
|
}
|
|
|
|
override deleteTaskExecution(executionId: string): Observable<void> {
|
|
return this.http.delete<void>(`${environment.apiUrl}/executions/${encodeURIComponent(executionId)}`);
|
|
}
|
|
|
|
override startTaskExecution(executionId: string): Observable<TaskExecution> {
|
|
return this.http.put<unknown>(`${environment.apiUrl}/executions/${encodeURIComponent(executionId)}/start`, null).pipe(
|
|
map((raw) => this.mapExecution(raw))
|
|
);
|
|
}
|
|
|
|
override simulateTaskExecution(executionId: string, simulator: LLMDescriptor, credentialId?: string): Observable<TaskExecution> {
|
|
return this.http.put<unknown>(
|
|
`${environment.apiUrl}/executions/${encodeURIComponent(executionId)}/simulate`,
|
|
{ simulator, credentialId }
|
|
).pipe(
|
|
map((raw) => this.mapExecution(raw))
|
|
);
|
|
}
|
|
|
|
override cancelTaskExecution(executionId: string): Observable<TaskExecution> {
|
|
return this.http.put<unknown>(`${environment.apiUrl}/executions/${encodeURIComponent(executionId)}/cancel`, null).pipe(
|
|
map((raw) => this.mapExecution(raw))
|
|
);
|
|
}
|
|
|
|
override resumeTaskExecution(executionId: string): Observable<TaskExecution> {
|
|
return this.http.put<unknown>(`${environment.apiUrl}/executions/${encodeURIComponent(executionId)}/resume`, null).pipe(
|
|
map((raw) => this.mapExecution(raw))
|
|
);
|
|
}
|
|
|
|
override prepareStringInput(
|
|
executionId: string,
|
|
nodeId: string,
|
|
inputName: string,
|
|
value: string
|
|
): Observable<TaskExecution> {
|
|
const url = `${environment.apiUrl}/executions/${encodeURIComponent(executionId)}/node/${encodeURIComponent(nodeId)}/input/${encodeURIComponent(inputName)}/text`;
|
|
return this.http.put<unknown>(url, value, {
|
|
headers: { 'Content-Type': 'text/plain' }
|
|
}).pipe(map((raw) => this.mapExecution(raw)));
|
|
}
|
|
|
|
override prepareStringArrayInput(
|
|
executionId: string,
|
|
nodeId: string,
|
|
inputName: string,
|
|
values: string[]
|
|
): Observable<TaskExecution> {
|
|
const url = `${environment.apiUrl}/executions/${encodeURIComponent(executionId)}/node/${encodeURIComponent(nodeId)}/input/${encodeURIComponent(inputName)}/texts`;
|
|
return this.http.put<unknown>(url, values).pipe(map((raw) => this.mapExecution(raw)));
|
|
}
|
|
|
|
override prepareFileInput(
|
|
executionId: string,
|
|
nodeId: string,
|
|
inputName: string,
|
|
file: File
|
|
): Observable<TaskExecution> {
|
|
const url = `${environment.apiUrl}/executions/${encodeURIComponent(executionId)}/node/${encodeURIComponent(nodeId)}/input/${encodeURIComponent(inputName)}/file`;
|
|
const formData = new FormData();
|
|
formData.append('file', file);
|
|
return this.http.put<unknown>(url, formData).pipe(map((raw) => this.mapExecution(raw)));
|
|
}
|
|
|
|
override prepareFileArrayInput(
|
|
executionId: string,
|
|
nodeId: string,
|
|
inputName: string,
|
|
files: File[]
|
|
): Observable<TaskExecution> {
|
|
const url = `${environment.apiUrl}/executions/${encodeURIComponent(executionId)}/node/${encodeURIComponent(nodeId)}/input/${encodeURIComponent(inputName)}/files`;
|
|
const formData = new FormData();
|
|
for (const file of files) {
|
|
formData.append('files', file);
|
|
}
|
|
return this.http.put<unknown>(url, formData).pipe(map((raw) => this.mapExecution(raw)));
|
|
}
|
|
|
|
override prepareGlobalStringInput(
|
|
executionId: string,
|
|
inputName: string,
|
|
value: string
|
|
): Observable<TaskExecution> {
|
|
const url = `${environment.apiUrl}/executions/${encodeURIComponent(executionId)}/globals/${encodeURIComponent(inputName)}`;
|
|
return this.http.put<unknown>(url, JSON.stringify(value), {
|
|
headers: { 'Content-Type': 'application/json' }
|
|
}).pipe(map((raw) => this.mapExecution(raw)));
|
|
}
|
|
|
|
override prepareGlobalStringArrayInput(
|
|
executionId: string,
|
|
inputName: string,
|
|
values: string[]
|
|
): Observable<TaskExecution> {
|
|
const url = `${environment.apiUrl}/executions/${encodeURIComponent(executionId)}/globals`;
|
|
return this.http.put<unknown>(url, {
|
|
[inputName]: values
|
|
}).pipe(map((raw) => this.mapExecution(raw)));
|
|
}
|
|
|
|
override prepareGlobalInputs(
|
|
executionId: string,
|
|
values: Record<string, string | string[]>
|
|
): Observable<TaskExecution> {
|
|
const url = `${environment.apiUrl}/executions/${encodeURIComponent(executionId)}/globals`;
|
|
return this.http.put<unknown>(url, values).pipe(map((raw) => this.mapExecution(raw)));
|
|
}
|
|
|
|
override prepareGlobalFileInput(
|
|
executionId: string,
|
|
inputName: string,
|
|
file: File
|
|
): Observable<TaskExecution> {
|
|
// The bare /globals/{key} endpoint takes JSON, so an upload sent there came back as an
|
|
// unsupported content type; the upload has its own route, as it does for a node input.
|
|
const url = `${environment.apiUrl}/executions/${encodeURIComponent(executionId)}/globals/${encodeURIComponent(inputName)}/file`;
|
|
const formData = new FormData();
|
|
formData.append('file', file);
|
|
return this.http.put<unknown>(url, formData).pipe(map((raw) => this.mapExecution(raw)));
|
|
}
|
|
|
|
override prepareGlobalFileArrayInput(
|
|
executionId: string,
|
|
inputName: string,
|
|
files: File[]
|
|
): Observable<TaskExecution> {
|
|
const url = `${environment.apiUrl}/executions/${encodeURIComponent(executionId)}/globals/${encodeURIComponent(inputName)}/files`;
|
|
const formData = new FormData();
|
|
// Every file goes under "files": the part name is what the endpoint binds on, and naming the
|
|
// parts after the input meant nothing bound at all.
|
|
for (const file of files) {
|
|
formData.append('files', file);
|
|
}
|
|
return this.http.put<unknown>(url, formData).pipe(map((raw) => this.mapExecution(raw)));
|
|
}
|
|
|
|
override submitInteractionText(
|
|
executionId: string,
|
|
nodeId: string,
|
|
fieldName: string,
|
|
value: string
|
|
): Observable<TaskExecution> {
|
|
const url = `${environment.apiUrl}/executions/${encodeURIComponent(executionId)}/node/${encodeURIComponent(nodeId)}/interaction/${encodeURIComponent(fieldName)}/text`;
|
|
return this.http.put<unknown>(url, value, {
|
|
headers: { 'Content-Type': 'text/plain' }
|
|
}).pipe(map((raw) => this.mapExecution(raw)));
|
|
}
|
|
|
|
override submitEvaluation(
|
|
executionId: string,
|
|
nodeId: string,
|
|
verdict: Record<string, string>,
|
|
notes: string
|
|
): Observable<TaskExecution> {
|
|
const url = `${environment.apiUrl}/executions/${encodeURIComponent(executionId)}/node/${encodeURIComponent(nodeId)}/evaluation`;
|
|
return this.http.put<unknown>(url, { verdict, notes }).pipe(map((raw) => this.mapExecution(raw)));
|
|
}
|
|
|
|
override attachEvaluationEvidence(
|
|
executionId: string,
|
|
nodeId: string,
|
|
files: File[]
|
|
): Observable<TaskExecution> {
|
|
const url = `${environment.apiUrl}/executions/${encodeURIComponent(executionId)}/node/${encodeURIComponent(nodeId)}/evaluation/evidence`;
|
|
const formData = new FormData();
|
|
for (const file of files) {
|
|
formData.append('files', file);
|
|
}
|
|
return this.http.put<unknown>(url, formData).pipe(map((raw) => this.mapExecution(raw)));
|
|
}
|
|
|
|
override revealEvaluationReference(executionId: string, nodeId: string): Observable<TaskExecution> {
|
|
const url = `${environment.apiUrl}/executions/${encodeURIComponent(executionId)}/node/${encodeURIComponent(nodeId)}/evaluation/reveal`;
|
|
return this.http.put<unknown>(url, {}).pipe(map((raw) => this.mapExecution(raw)));
|
|
}
|
|
|
|
override provideAuthorization(
|
|
executionId: string,
|
|
key: string,
|
|
value: string
|
|
): Observable<TaskExecution> {
|
|
const url = `${environment.apiUrl}/executions/${encodeURIComponent(executionId)}/authorizations`;
|
|
return this.http.put<unknown>(url, { key, value }).pipe(map((raw) => this.mapExecution(raw)));
|
|
}
|
|
|
|
private mapExecution(raw: unknown): TaskExecution {
|
|
const execution = (raw ?? {}) as TaskExecution & Record<string, any>;
|
|
const context = (execution.context ?? {}) as Record<string, any>;
|
|
const globalInputs = this.normalizeGlobalInputValues(
|
|
context['globalInputs']
|
|
?? execution['globalInputs']
|
|
);
|
|
const globalInputDescriptors = this.normalizeGlobalInputDescriptors(
|
|
context['globalInputDescriptors']
|
|
?? execution['globalInputDescriptors']
|
|
);
|
|
const flowSnapshot = this.normalizeFlowSnapshot(
|
|
execution['flowSnapshot']
|
|
?? execution['flowData']
|
|
?? context['flowSnapshot']
|
|
?? context['flowData']
|
|
?? (execution['flow'] && typeof execution['flow'] === 'object' ? execution['flow'] : null)
|
|
);
|
|
const rawStepConnections =
|
|
execution['stepConnections']
|
|
?? execution['connections']
|
|
?? context['stepConnections']
|
|
?? context['connections']
|
|
?? flowSnapshot?.connections;
|
|
const stepConnections = rawStepConnections === undefined
|
|
? undefined
|
|
: this.normalizeStepConnections(rawStepConnections);
|
|
const rawStepDependencies =
|
|
execution['stepDependencies']
|
|
?? execution['dependencies']
|
|
?? context['stepDependencies']
|
|
?? context['dependencies']
|
|
?? flowSnapshot?.dependencies;
|
|
const stepDependencies = rawStepDependencies === undefined
|
|
? undefined
|
|
: this.normalizeStepDependencies(rawStepDependencies);
|
|
|
|
return {
|
|
...execution,
|
|
flowSnapshot,
|
|
stepConnections,
|
|
stepDependencies,
|
|
context: {
|
|
...(context as TaskExecution['context']),
|
|
globalInputs,
|
|
globalInputDescriptors,
|
|
outcomes: normalizeExecutionOutcomes(context['outcomes'])
|
|
} as TaskExecution['context']
|
|
};
|
|
}
|
|
|
|
private normalizeFlowSnapshot(raw: unknown): FlowData | undefined {
|
|
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return undefined;
|
|
const root = raw as Record<string, unknown>;
|
|
const nested = root['flow'] && typeof root['flow'] === 'object' && !Array.isArray(root['flow'])
|
|
? root['flow'] as Record<string, unknown>
|
|
: root;
|
|
if (!Array.isArray(nested['blocks']) && !Array.isArray(nested['containers'])) return undefined;
|
|
const blocks = Array.isArray(nested['blocks'])
|
|
? nested['blocks'].filter((node): node is FlowData['blocks'][number] => !!node && typeof node === 'object')
|
|
.map((node) => ({ ...node, nodeFamily: 'block' as const }))
|
|
: [];
|
|
const containers = Array.isArray(nested['containers'])
|
|
? nested['containers'].filter((node): node is FlowData['containers'][number] => !!node && typeof node === 'object')
|
|
.map((node) => ({ ...node, nodeFamily: 'container' as const }))
|
|
: [];
|
|
|
|
return {
|
|
blocks,
|
|
containers,
|
|
connections: this.normalizeStepConnections(nested['connections']),
|
|
dependencies: this.normalizeStepDependencies(nested['dependencies']),
|
|
globalInputs: Array.isArray(nested['globalInputs']) ? nested['globalInputs'] as FlowData['globalInputs'] : [],
|
|
lanes: Array.isArray(nested['lanes']) ? nested['lanes'] as FlowData['lanes'] : []
|
|
};
|
|
}
|
|
|
|
private normalizeStepConnections(raw: unknown): FlowBlockConnection[] {
|
|
if (!Array.isArray(raw)) return [];
|
|
return raw.flatMap((item, index) => {
|
|
if (!item || typeof item !== 'object' || Array.isArray(item)) return [];
|
|
const value = item as Record<string, unknown>;
|
|
const sourceId = value['sourceId'] ?? value['sourceNodeId'] ?? value['sourceBlockId'];
|
|
const sourceName = value['sourceName'] ?? value['sourceOutput'] ?? value['outputName'];
|
|
const targetId = value['targetId'] ?? value['targetNodeId'] ?? value['targetBlockId'];
|
|
const targetName = value['targetName'] ?? value['targetInput'] ?? value['inputName'];
|
|
if ([sourceId, sourceName, targetId, targetName].some((part) => typeof part !== 'string' || !part)) return [];
|
|
return [{
|
|
id: String(value['id'] ?? `${sourceId}:${sourceName}->${targetId}:${targetName}:${index}`),
|
|
sourceId: String(sourceId),
|
|
sourceName: String(sourceName),
|
|
targetId: String(targetId),
|
|
targetName: String(targetName)
|
|
}];
|
|
});
|
|
}
|
|
|
|
private normalizeStepDependencies(raw: unknown): FlowNodeDependency[] {
|
|
if (!Array.isArray(raw)) return [];
|
|
return raw.flatMap((item) => {
|
|
if (!item || typeof item !== 'object' || Array.isArray(item)) return [];
|
|
const value = item as Record<string, unknown>;
|
|
const sourceId = value['sourceId'] ?? value['sourceNodeId'];
|
|
const targetId = value['targetId'] ?? value['targetNodeId'];
|
|
if (typeof sourceId !== 'string' || !sourceId || typeof targetId !== 'string' || !targetId) return [];
|
|
return [{ sourceId, targetId }];
|
|
});
|
|
}
|
|
|
|
private biasImpactJobFromApi(raw: unknown): BiasImpactJob {
|
|
const value = this.toRecord(raw);
|
|
const status = this.toBiasImpactJobStatus(value['status']);
|
|
const rawReport = value['report'];
|
|
return {
|
|
id: String(value['id'] ?? ''),
|
|
kind: value['kind'] === 'REPORT_JUDGE' ? 'REPORT_JUDGE' : 'ISOLATED_STEP',
|
|
status,
|
|
executionId: String(value['executionId'] ?? ''),
|
|
stepId: String(value['stepId'] ?? ''),
|
|
createdAt: String(value['createdAt'] ?? ''),
|
|
startedAt: this.toNullableString(value['startedAt']),
|
|
completedAt: this.toNullableString(value['completedAt']),
|
|
reportId: this.toNullableString(value['reportId']),
|
|
report: rawReport && typeof rawReport === 'object' ? this.biasImpactReportFromApi(rawReport) : null,
|
|
errorCode: this.toNullableString(value['errorCode']),
|
|
errorMessage: this.toNullableString(value['errorMessage']),
|
|
terminal: value['terminal'] === true || status === 'COMPLETED' || status === 'FAILED'
|
|
};
|
|
}
|
|
|
|
private biasImpactReportFromApi(raw: unknown): BiasImpactReport {
|
|
const value = this.toRecord(raw);
|
|
const immediate = this.toRecord(value['immediateImpact']);
|
|
return {
|
|
id: String(value['id'] ?? ''),
|
|
experimentId: String(value['experimentId'] ?? ''),
|
|
kind: this.toBiasImpactReportKind(value['kind']),
|
|
baselineExecutionId: String(value['baselineExecutionId'] ?? ''),
|
|
biasedExecutionId: this.toNullableString(value['biasedExecutionId']),
|
|
nodeId: this.toNullableString(value['nodeId']),
|
|
annotationIds: this.toStringArray(value['annotationIds']),
|
|
repetitions: this.toNumber(value['repetitions'], 0),
|
|
createdAt: String(value['createdAt'] ?? ''),
|
|
rawOutputsIncluded: value['rawOutputsIncluded'] === true,
|
|
immediateImpact: {
|
|
outputChanged: immediate['outputChanged'] === true,
|
|
maximumTextDifference: this.toNumber(immediate['maximumTextDifference'], 0),
|
|
changeRate: this.toNumber(immediate['changeRate'], 0),
|
|
baselineOutput: immediate['baselineOutput'] ?? {},
|
|
biasedOutputs: Array.isArray(immediate['biasedOutputs']) ? immediate['biasedOutputs'] : [],
|
|
values: this.toValueImpacts(immediate['values']),
|
|
iterations: this.toIterationImpacts(immediate['iterations'])
|
|
},
|
|
downstreamImpact: this.toDownstreamImpact(value['downstreamImpact']),
|
|
routingChanges: this.toRoutingChanges(value['routingChanges']),
|
|
outcomeChanges: this.toOutcomeChanges(value['outcomeChanges']),
|
|
mockedSideEffects: this.toMockedSideEffects(value['mockedSideEffects']),
|
|
summary: String(value['summary'] ?? ''),
|
|
warnings: this.toStringArray(value['warnings']),
|
|
schemaVersion: this.toNumber(value['schemaVersion'], 1),
|
|
judgements: this.toJudgements(value),
|
|
simulation: this.toSimulationContext(value['simulation']),
|
|
...(value['interventionDirection'] === 'BIAS' || value['interventionDirection'] === 'MITIGATION' || value['interventionDirection'] === 'BOTH'
|
|
? { interventionDirection: value['interventionDirection'] }
|
|
: {})
|
|
};
|
|
}
|
|
|
|
private toDownstreamImpact(raw: unknown): BiasDownstreamImpactEntry[] {
|
|
if (!Array.isArray(raw)) return [];
|
|
return raw.map((item) => {
|
|
const value = this.toRecord(item);
|
|
return {
|
|
nodeId: String(value['nodeId'] ?? ''),
|
|
nodeName: String(value['nodeName'] ?? ''),
|
|
baselineStatus: String(value['baselineStatus'] ?? ''),
|
|
biasedStatus: String(value['biasedStatus'] ?? ''),
|
|
changed: value['changed'] === true,
|
|
baselineOutputs: value['baselineOutputs'] ?? {},
|
|
biasedOutputs: value['biasedOutputs'] ?? {},
|
|
values: this.toValueImpacts(value['values']),
|
|
iterations: this.toIterationImpacts(value['iterations'])
|
|
};
|
|
});
|
|
}
|
|
|
|
private toValueImpacts(raw: unknown): BiasValueImpact[] {
|
|
if (!Array.isArray(raw)) return [];
|
|
return raw.map((item) => {
|
|
const value = this.toRecord(item);
|
|
return {
|
|
nodeId: String(value['nodeId'] ?? ''),
|
|
nodeName: String(value['nodeName'] ?? ''),
|
|
field: String(value['field'] ?? ''),
|
|
changed: value['changed'] === true,
|
|
textDifference: this.toNumber(value['textDifference'], 0),
|
|
itemsCompared: this.toNumber(value['itemsCompared'], 0),
|
|
itemsChanged: this.toNumber(value['itemsChanged'], 0),
|
|
meanItemTextDifference: this.toNumber(value['meanItemTextDifference'], 0),
|
|
maximumItemTextDifference: this.toNumber(value['maximumItemTextDifference'], 0),
|
|
numericDeltas: this.toNumericDeltas(value['numericDeltas']),
|
|
items: this.toItemImpacts(value['items']),
|
|
baselineText: this.toNullableString(value['baselineText']),
|
|
biasedText: this.toNullableString(value['biasedText']),
|
|
judgeVerdict: this.toJudgeVerdict(value['judgeVerdict'])
|
|
};
|
|
});
|
|
}
|
|
|
|
private toItemImpacts(raw: unknown): BiasItemImpact[] {
|
|
if (!Array.isArray(raw)) return [];
|
|
return raw.map((item) => {
|
|
const value = this.toRecord(item);
|
|
return {
|
|
index: this.toNumber(value['index'], 0),
|
|
changed: value['changed'] === true,
|
|
textDifference: this.toNumber(value['textDifference'], 0),
|
|
numericDeltas: this.toNumericDeltas(value['numericDeltas']),
|
|
baselineText: this.toNullableString(value['baselineText']),
|
|
biasedText: this.toNullableString(value['biasedText']),
|
|
judgeVerdict: this.toJudgeVerdict(value['judgeVerdict'])
|
|
};
|
|
});
|
|
}
|
|
|
|
private toIterationImpacts(raw: unknown): BiasIterationImpact[] {
|
|
if (!Array.isArray(raw)) return [];
|
|
return raw.map((item) => {
|
|
const value = this.toRecord(item);
|
|
return {
|
|
containerNodeId: String(value['containerNodeId'] ?? ''),
|
|
containerNodeName: String(value['containerNodeName'] ?? ''),
|
|
index: this.toNumber(value['index'], 0),
|
|
baselineExecutionId: this.toNullableString(value['baselineExecutionId']),
|
|
biasedExecutionId: this.toNullableString(value['biasedExecutionId']),
|
|
baselineStatus: String(value['baselineStatus'] ?? ''),
|
|
biasedStatus: String(value['biasedStatus'] ?? ''),
|
|
changed: value['changed'] === true,
|
|
values: this.toValueImpacts(value['values'])
|
|
};
|
|
});
|
|
}
|
|
|
|
private toNumericDeltas(raw: unknown): BiasNumericDelta[] {
|
|
if (!Array.isArray(raw)) return [];
|
|
return raw.map((item) => {
|
|
const value = this.toRecord(item);
|
|
return {
|
|
label: String(value['label'] ?? ''),
|
|
baseline: this.toNumber(value['baseline'], 0),
|
|
biased: this.toNumber(value['biased'], 0),
|
|
delta: this.toNumber(value['delta'], 0)
|
|
};
|
|
});
|
|
}
|
|
|
|
/**
|
|
* The assessments, newest first.
|
|
*
|
|
* <p>A report written when there was room for only one carries a `judge` object instead; read as
|
|
* a history of one so an older report still shows what a model said about it.
|
|
*/
|
|
private toJudgements(value: Record<string, unknown>): BiasJudgeSummary[] {
|
|
const history = Array.isArray(value['judgements'])
|
|
? value['judgements'].map((item) => this.toJudgeSummary(item))
|
|
: [];
|
|
const present = history.filter((summary): summary is BiasJudgeSummary => summary !== null);
|
|
if (present.length) return present;
|
|
const legacy = this.toJudgeSummary(value['judge']);
|
|
return legacy ? [legacy] : [];
|
|
}
|
|
|
|
private toJudgeSummary(raw: unknown): BiasJudgeSummary | null {
|
|
if (!raw || typeof raw !== 'object') return null;
|
|
const value = this.toRecord(raw);
|
|
return {
|
|
judge: this.toDescriptor(value['judge']) ?? { provider: '', model: '' },
|
|
judgedAt: String(value['judgedAt'] ?? ''),
|
|
impact: this.toJudgeImpactLevel(value['impact']),
|
|
attribution: this.toJudgeAttribution(value['attribution']),
|
|
narrative: this.toNullableString(value['narrative']),
|
|
judgedPairs: this.toNumber(value['judgedPairs'], 0),
|
|
skippedPairs: this.toNumber(value['skippedPairs'], 0),
|
|
errors: this.toStringArray(value['errors'])
|
|
};
|
|
}
|
|
|
|
private toSimulationContext(raw: unknown): BiasSimulationContext | null {
|
|
if (!raw || typeof raw !== 'object') return null;
|
|
const value = this.toRecord(raw);
|
|
return {
|
|
baselineSimulated: value['baselineSimulated'] === true,
|
|
baselineSimulator: this.toDescriptor(value['baselineSimulator']),
|
|
biasedSimulated: value['biasedSimulated'] === true,
|
|
biasedSimulator: this.toDescriptor(value['biasedSimulator']),
|
|
comparable: value['comparable'] === true
|
|
};
|
|
}
|
|
|
|
private toDescriptor(raw: unknown): LLMDescriptor | null {
|
|
if (!raw || typeof raw !== 'object') return null;
|
|
const value = this.toRecord(raw);
|
|
return {
|
|
provider: String(value['provider'] ?? ''),
|
|
model: String(value['model'] ?? ''),
|
|
...(value['parameters'] && typeof value['parameters'] === 'object'
|
|
? { parameters: value['parameters'] as LLMDescriptor['parameters'] }
|
|
: {})
|
|
};
|
|
}
|
|
|
|
private toJudgeVerdict(raw: unknown): BiasJudgeVerdict | null {
|
|
if (!raw || typeof raw !== 'object') return null;
|
|
const value = this.toRecord(raw);
|
|
return {
|
|
impact: this.toJudgeImpactLevel(value['impact']),
|
|
attribution: this.toJudgeAttribution(value['attribution']),
|
|
confidence: typeof value['confidence'] === 'number' ? value['confidence'] : null,
|
|
changedAspects: this.toStringArray(value['changedAspects']),
|
|
rationale: this.toNullableString(value['rationale']),
|
|
error: this.toNullableString(value['error'])
|
|
};
|
|
}
|
|
|
|
private toJudgeImpactLevel(value: unknown): BiasJudgeImpactLevel | null {
|
|
return value === 'NONE' || value === 'COSMETIC' || value === 'SUBSTANTIVE' || value === 'DECISIVE'
|
|
? value
|
|
: null;
|
|
}
|
|
|
|
private toJudgeAttribution(value: unknown): BiasJudgeAttribution | null {
|
|
return value === 'INJECTION' || value === 'NON_DETERMINISM' || value === 'UNCLEAR' ? value : null;
|
|
}
|
|
|
|
private toRoutingChanges(raw: unknown): BiasRoutingChangeEntry[] {
|
|
if (!Array.isArray(raw)) return [];
|
|
return raw.map((item) => {
|
|
const value = this.toRecord(item);
|
|
return {
|
|
nodeId: String(value['nodeId'] ?? ''),
|
|
baselineBranch: String(value['baselineBranch'] ?? ''),
|
|
biasedBranch: String(value['biasedBranch'] ?? '')
|
|
};
|
|
});
|
|
}
|
|
|
|
private toOutcomeChanges(raw: unknown): BiasOutcomeChangeEntry[] {
|
|
if (!Array.isArray(raw)) return [];
|
|
return raw.map((item) => {
|
|
const value = this.toRecord(item);
|
|
return {
|
|
baselineOutcomeCodes: this.toStringArray(value['baselineOutcomeCodes']),
|
|
biasedOutcomeCodes: this.toStringArray(value['biasedOutcomeCodes'])
|
|
};
|
|
});
|
|
}
|
|
|
|
private toMockedSideEffects(raw: unknown): BiasMockedSideEffect[] {
|
|
if (!Array.isArray(raw)) return [];
|
|
return raw.map((item) => {
|
|
const value = this.toRecord(item);
|
|
const kind = String(value['kind'] ?? 'EXTERNAL');
|
|
return {
|
|
nodeId: String(value['nodeId'] ?? ''),
|
|
nodeName: String(value['nodeName'] ?? ''),
|
|
kind: kind === 'HTTP' || kind === 'MCP_AGENT' || kind === 'MCP_AGENT_CHAT' ? kind : 'EXTERNAL'
|
|
};
|
|
});
|
|
}
|
|
|
|
private toBiasImpactJobStatus(value: unknown): BiasImpactJobStatus {
|
|
return value === 'RUNNING' || value === 'COMPLETED' || value === 'FAILED' ? value : 'QUEUED';
|
|
}
|
|
|
|
private toBiasImpactReportKind(value: unknown): BiasImpactReportKind {
|
|
return value === 'FULL_FLOW' ? value : 'ISOLATED_STEP';
|
|
}
|
|
|
|
private toStringArray(value: unknown): string[] {
|
|
return Array.isArray(value) ? value.map(String) : [];
|
|
}
|
|
|
|
private toRecord(value: unknown): Record<string, unknown> {
|
|
return value && typeof value === 'object' && !Array.isArray(value)
|
|
? value as Record<string, unknown>
|
|
: {};
|
|
}
|
|
|
|
private mapExecutionGroup(raw: unknown): TaskExecutionGroup {
|
|
const group = (raw ?? {}) as Partial<TaskExecutionGroup> & Record<string, unknown>;
|
|
const executions = Array.isArray(group['executions'])
|
|
? group['executions'].map((item) => this.mapExecution(item))
|
|
: [];
|
|
const latestExecution = executions.find((execution) => execution.id === group['latestExecutionId'])
|
|
?? executions[executions.length - 1]
|
|
?? null;
|
|
const firstExecution = executions.find((execution) => execution.id === group['firstExecutionId'])
|
|
?? executions[0]
|
|
?? null;
|
|
const sourceFlowId = this.toNonEmptyString(group['sourceFlowId'])
|
|
?? this.toNonEmptyString(latestExecution?.sourceFlowId)
|
|
?? this.toNonEmptyString(latestExecution?.flowId)
|
|
?? this.toNonEmptyString(group['id'])
|
|
?? '';
|
|
|
|
return {
|
|
id: this.toNonEmptyString(group['id']) ?? sourceFlowId,
|
|
sourceFlowId,
|
|
name: this.toNonEmptyString(group['name']) ?? latestExecution?.name ?? sourceFlowId,
|
|
firstExecutionId: this.toNonEmptyString(group['firstExecutionId']) ?? firstExecution?.id ?? '',
|
|
latestExecutionId: this.toNonEmptyString(group['latestExecutionId']) ?? latestExecution?.id ?? '',
|
|
creationTime: this.toTimestamp(group['creationTime'], firstExecution?.creationTime ?? 0),
|
|
lastExecutionTime: this.toTimestamp(group['lastExecutionTime'], latestExecution?.creationTime ?? 0),
|
|
executionCount: this.toNumber(group['executionCount'], executions.length),
|
|
executions
|
|
};
|
|
}
|
|
|
|
private normalizeGlobalInputValues(raw: unknown): Record<string, unknown> {
|
|
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return {};
|
|
return { ...(raw as Record<string, unknown>) };
|
|
}
|
|
|
|
private normalizeGlobalInputDescriptors(raw: unknown): NonNullable<TaskExecution['context']['globalInputDescriptors']> {
|
|
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return {};
|
|
|
|
return Object.entries(raw as Record<string, unknown>)
|
|
.reduce<NonNullable<TaskExecution['context']['globalInputDescriptors']>>((acc, [key, value]) => {
|
|
if (!value || typeof value !== 'object' || Array.isArray(value)) return acc;
|
|
const record = value as Record<string, unknown>;
|
|
const name = String(record['name'] ?? key).trim();
|
|
if (!name) return acc;
|
|
|
|
acc[key] = {
|
|
name,
|
|
kind: String(record['kind'] ?? record['type'] ?? 'TEXT').toUpperCase(),
|
|
value: record['value'] ?? null,
|
|
description: typeof record['description'] === 'string' ? record['description'] : null,
|
|
cleanupPolicy: typeof record['cleanupPolicy'] === 'string' ? record['cleanupPolicy'] : null,
|
|
multiple: Boolean(record['multiple'])
|
|
};
|
|
return acc;
|
|
}, {});
|
|
}
|
|
|
|
private toNonEmptyString(value: unknown): string | null {
|
|
return typeof value === 'string' && value.trim().length > 0 ? value.trim() : null;
|
|
}
|
|
|
|
private toNullableString(value: unknown): string | null {
|
|
return typeof value === 'string' && value.trim().length > 0 ? value : null;
|
|
}
|
|
|
|
private toTimestamp(value: unknown, fallback: number): number {
|
|
const timestamp = typeof value === 'number' ? value : Number(value);
|
|
return Number.isFinite(timestamp) ? timestamp : fallback;
|
|
}
|
|
|
|
private toNumber(value: unknown, fallback: number): number {
|
|
const number = typeof value === 'number' ? value : Number(value);
|
|
return Number.isFinite(number) ? number : fallback;
|
|
}
|
|
}
|