feat(assistant): add provider and model selection
This commit is contained in:
parent
b6474def51
commit
cf6b25bd1d
|
|
@ -41,9 +41,43 @@ export type AssistantEditorDraft = {
|
|||
};
|
||||
|
||||
export type AssistantConfig = {
|
||||
provider?: string;
|
||||
defaultProvider: string;
|
||||
defaultModel: string;
|
||||
availableProvidersRetrieverUrl: string;
|
||||
availableModelsRetrieverUrl: string;
|
||||
defaultPhaseModels: AssistantPhaseModels;
|
||||
};
|
||||
|
||||
export type AssistantPhaseModels = {
|
||||
planningModel?: string;
|
||||
jsonModel?: string;
|
||||
repairModel?: string;
|
||||
};
|
||||
|
||||
export type AssistantLlmSelection = {
|
||||
provider: string;
|
||||
model: string;
|
||||
phaseModels?: AssistantPhaseModels;
|
||||
};
|
||||
|
||||
export type AssistantSessionRequest = {
|
||||
llmSelection?: AssistantLlmSelection;
|
||||
};
|
||||
|
||||
export type AssistantFlowRequest = {
|
||||
userPrompt: string;
|
||||
llmSelection?: AssistantLlmSelection;
|
||||
flow?: AssistantDraftPayload;
|
||||
validationErrors?: AssistantValidationIssue[];
|
||||
maxRepairAttempts?: number;
|
||||
};
|
||||
|
||||
export type AssistantFlowActionResult = {
|
||||
flow: AssistantDraftPayload | null;
|
||||
valid?: boolean;
|
||||
validationErrors: AssistantValidationIssue[];
|
||||
warnings: string[];
|
||||
message: string;
|
||||
};
|
||||
|
||||
export type AssistantCallStatus = 'QUEUED' | 'RUNNING' | 'COMPLETED' | 'FAILED' | 'CANCELLED';
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
import {
|
||||
AssistantCallState,
|
||||
AssistantConfig,
|
||||
AssistantSendMessageRequest,
|
||||
AssistantFlowActionResult,
|
||||
AssistantFlowRequest,
|
||||
AssistantSessionRequest,
|
||||
AssistantSessionState
|
||||
} from '@models/assistant';
|
||||
import { Observable } from 'rxjs';
|
||||
|
|
@ -9,17 +10,18 @@ import { Observable } from 'rxjs';
|
|||
export abstract class AssistantCallServiceBase {
|
||||
abstract getConfig(): Observable<AssistantConfig>;
|
||||
|
||||
abstract listModels(retrieverUrl: string): Observable<string[]>;
|
||||
abstract listProviders(retrieverUrl: string): Observable<string[]>;
|
||||
|
||||
abstract createSession(request: {
|
||||
model: string;
|
||||
}): Observable<AssistantSessionState>;
|
||||
abstract listModels(retrieverUrlTemplate: string, provider: string): Observable<string[]>;
|
||||
|
||||
abstract sendMessage(sessionId: string, request: AssistantSendMessageRequest): Observable<{ callId: string }>;
|
||||
abstract createSession(request: AssistantSessionRequest): Observable<AssistantSessionState>;
|
||||
|
||||
abstract getCall(callId: string): Observable<AssistantCallState>;
|
||||
abstract draft(request: AssistantFlowRequest): Observable<AssistantFlowActionResult>;
|
||||
|
||||
abstract cancelCall(callId: string): Observable<AssistantCallState>;
|
||||
abstract refine(request: AssistantFlowRequest): Observable<AssistantFlowActionResult>;
|
||||
|
||||
abstract fix(request: AssistantFlowRequest): Observable<AssistantFlowActionResult>;
|
||||
|
||||
abstract explain(request: AssistantFlowRequest): Observable<AssistantFlowActionResult>;
|
||||
|
||||
abstract getSession(sessionId: string): Observable<AssistantSessionState>;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
import { catchError, of } from 'rxjs';
|
||||
import { AssistantCallServiceFake } from './assistant-call.fake';
|
||||
|
||||
describe('AssistantCallServiceFake', () => {
|
||||
|
|
@ -8,37 +7,17 @@ describe('AssistantCallServiceFake', () => {
|
|||
service = new AssistantCallServiceFake();
|
||||
});
|
||||
|
||||
it('surfaces "call not found" as an observable error catchError can intercept, not a synchronous throw', async () => {
|
||||
expect(() => service.getCall('missing-call')).not.toThrow();
|
||||
|
||||
let caught: unknown = null;
|
||||
await new Promise<void>((resolve) => {
|
||||
service.getCall('missing-call').pipe(
|
||||
catchError((error) => {
|
||||
caught = error;
|
||||
return of(null);
|
||||
})
|
||||
).subscribe(() => resolve());
|
||||
it('uses the selected model only when a custom selection is supplied', async () => {
|
||||
let selectedModel = '';
|
||||
service.draft({
|
||||
userPrompt: 'Create a flow',
|
||||
llmSelection: { provider: 'OpenAI', model: 'custom-model' }
|
||||
}).subscribe((result) => {
|
||||
selectedModel = String(
|
||||
(result.flow?.flow.blocks[1]?.specificConfiguration as Record<string, any>)['llmDescriptor']?.model
|
||||
);
|
||||
});
|
||||
|
||||
expect(caught).toBeInstanceOf(Error);
|
||||
expect((caught as Error).message).toContain('missing-call');
|
||||
});
|
||||
|
||||
it('surfaces "session not found" as an observable error catchError can intercept, not a synchronous throw', async () => {
|
||||
expect(() => service.getSession('missing-session')).not.toThrow();
|
||||
|
||||
let caught: unknown = null;
|
||||
await new Promise<void>((resolve) => {
|
||||
service.getSession('missing-session').pipe(
|
||||
catchError((error) => {
|
||||
caught = error;
|
||||
return of(null);
|
||||
})
|
||||
).subscribe(() => resolve());
|
||||
});
|
||||
|
||||
expect(caught).toBeInstanceOf(Error);
|
||||
expect((caught as Error).message).toContain('missing-session');
|
||||
expect(selectedModel).toBe('custom-model');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,50 +1,41 @@
|
|||
import {
|
||||
AssistantCallPhase,
|
||||
AssistantCallState,
|
||||
AssistantChatMessage,
|
||||
AssistantConfig,
|
||||
AssistantDraftPayload,
|
||||
AssistantSendMessageRequest,
|
||||
AssistantSessionState,
|
||||
AssistantValidationIssue
|
||||
AssistantFlowActionResult,
|
||||
AssistantFlowRequest,
|
||||
AssistantSessionRequest,
|
||||
AssistantSessionState
|
||||
} from '@models/assistant';
|
||||
import { FlowData } from '@models/flow';
|
||||
import { defer, Observable, of } from 'rxjs';
|
||||
import { Observable, of } from 'rxjs';
|
||||
import { AssistantCallServiceBase } from './assistant-call.base';
|
||||
|
||||
type FakeCallRecord = {
|
||||
id: string;
|
||||
sessionId: string;
|
||||
content: string;
|
||||
phaseIndex: number;
|
||||
phases: AssistantCallPhase[];
|
||||
completed: boolean;
|
||||
failed: boolean;
|
||||
cancelled: boolean;
|
||||
};
|
||||
|
||||
export class AssistantCallServiceFake extends AssistantCallServiceBase {
|
||||
private readonly models = ['llama3.1:8b', 'qwen2.5:7b', 'mistral:7b'];
|
||||
private readonly sessions = new Map<string, AssistantSessionState>();
|
||||
private readonly calls = new Map<string, FakeCallRecord>();
|
||||
private readonly providers = ['InternalOllama', 'OpenAI'];
|
||||
|
||||
override getConfig(): Observable<AssistantConfig> {
|
||||
return of({
|
||||
defaultProvider: this.providers[0],
|
||||
defaultModel: this.models[0],
|
||||
availableModelsRetrieverUrl: '/fake/assistant/models'
|
||||
availableProvidersRetrieverUrl: '/fake/assistant/providers',
|
||||
availableModelsRetrieverUrl: '/fake/assistant/models?provider={provider}',
|
||||
defaultPhaseModels: {}
|
||||
});
|
||||
}
|
||||
|
||||
override listModels(_retrieverUrl: string): Observable<string[]> {
|
||||
override listProviders(_retrieverUrl: string): Observable<string[]> {
|
||||
return of(this.providers);
|
||||
}
|
||||
|
||||
override listModels(_retrieverUrlTemplate: string, _provider: string): Observable<string[]> {
|
||||
return of(this.models);
|
||||
}
|
||||
|
||||
override createSession(request: {
|
||||
model: string;
|
||||
}): Observable<AssistantSessionState> {
|
||||
override createSession(request: AssistantSessionRequest): Observable<AssistantSessionState> {
|
||||
const session: AssistantSessionState = {
|
||||
id: crypto.randomUUID(),
|
||||
selectedModel: request.model,
|
||||
selectedModel: request.llmSelection?.model ?? this.models[0],
|
||||
messages: [
|
||||
{
|
||||
id: crypto.randomUUID(),
|
||||
|
|
@ -57,187 +48,55 @@ export class AssistantCallServiceFake extends AssistantCallServiceBase {
|
|||
lastValidationErrors: [],
|
||||
lastCallId: null
|
||||
};
|
||||
this.sessions.set(session.id, session);
|
||||
return of(structuredClone(session));
|
||||
}
|
||||
|
||||
override sendMessage(sessionId: string, request: AssistantSendMessageRequest): Observable<{ callId: string }> {
|
||||
return defer(() => {
|
||||
const session = this.sessions.get(sessionId);
|
||||
if (!session) {
|
||||
throw new Error(`Assistant session ${sessionId} not found`);
|
||||
}
|
||||
session.messages = [
|
||||
...session.messages,
|
||||
{
|
||||
id: crypto.randomUUID(),
|
||||
role: 'user',
|
||||
content: request.message
|
||||
}
|
||||
];
|
||||
|
||||
const callId = crypto.randomUUID();
|
||||
const phases = this.buildPhases(request.message);
|
||||
this.calls.set(callId, {
|
||||
id: callId,
|
||||
sessionId,
|
||||
content: request.message,
|
||||
phaseIndex: 0,
|
||||
phases,
|
||||
completed: false,
|
||||
failed: false,
|
||||
cancelled: false
|
||||
});
|
||||
session.lastCallId = callId;
|
||||
|
||||
return of({ callId });
|
||||
});
|
||||
}
|
||||
|
||||
override getCall(callId: string): Observable<AssistantCallState> {
|
||||
return defer(() => {
|
||||
const call = this.calls.get(callId);
|
||||
if (!call) {
|
||||
throw new Error(`Assistant call ${callId} not found`);
|
||||
}
|
||||
|
||||
if (!call.completed && !call.failed && !call.cancelled) {
|
||||
if (call.phaseIndex < call.phases.length - 1) {
|
||||
call.phaseIndex += 1;
|
||||
} else {
|
||||
call.completed = true;
|
||||
this.applyCallResult(call);
|
||||
}
|
||||
}
|
||||
|
||||
const phase = call.completed
|
||||
? 'completed'
|
||||
: call.failed
|
||||
? 'failed'
|
||||
: call.cancelled
|
||||
? 'cancelled'
|
||||
: call.phases[call.phaseIndex];
|
||||
|
||||
const result: AssistantCallState = {
|
||||
id: call.id,
|
||||
sessionId: call.sessionId,
|
||||
status: call.failed
|
||||
? 'FAILED'
|
||||
: call.cancelled
|
||||
? 'CANCELLED'
|
||||
: call.completed
|
||||
? 'COMPLETED'
|
||||
: call.phaseIndex === 0
|
||||
? 'QUEUED'
|
||||
: 'RUNNING',
|
||||
phase,
|
||||
progressMessage: call.cancelled ? 'Assistant request cancelled' : undefined,
|
||||
errorMessage: call.failed ? 'Fake assistant call failed.' : undefined
|
||||
};
|
||||
return of(result);
|
||||
});
|
||||
}
|
||||
|
||||
override cancelCall(callId: string): Observable<AssistantCallState> {
|
||||
return defer(() => {
|
||||
const call = this.calls.get(callId);
|
||||
if (!call) {
|
||||
throw new Error(`Assistant call ${callId} not found`);
|
||||
}
|
||||
|
||||
call.cancelled = true;
|
||||
const result: AssistantCallState = {
|
||||
id: call.id,
|
||||
sessionId: call.sessionId,
|
||||
status: 'CANCELLED',
|
||||
phase: 'cancelled',
|
||||
progressMessage: 'Assistant request cancelled'
|
||||
};
|
||||
return of(result);
|
||||
});
|
||||
}
|
||||
|
||||
override getSession(sessionId: string): Observable<AssistantSessionState> {
|
||||
return defer(() => {
|
||||
const session = this.sessions.get(sessionId);
|
||||
if (!session) {
|
||||
throw new Error(`Assistant session ${sessionId} not found`);
|
||||
}
|
||||
return of(structuredClone(session));
|
||||
});
|
||||
}
|
||||
|
||||
private buildPhases(content: string): AssistantCallPhase[] {
|
||||
const normalized = content.toLowerCase();
|
||||
if (/explain|what does|why/.test(normalized)) {
|
||||
return ['routing', 'explaining'];
|
||||
}
|
||||
if (/fix|repair|invalid|error|bug/.test(normalized)) {
|
||||
return ['routing', 'planning', 'configuring_blocks', 'connecting_blocks', 'validating', 'fixing'];
|
||||
}
|
||||
return ['routing', 'planning', 'configuring_blocks', 'connecting_blocks', 'validating'];
|
||||
}
|
||||
|
||||
private applyCallResult(call: FakeCallRecord) {
|
||||
const session = this.sessions.get(call.sessionId);
|
||||
if (!session) return;
|
||||
|
||||
const content = call.content.toLowerCase();
|
||||
|
||||
if (/explain|what does|why/.test(content)) {
|
||||
session.messages = [
|
||||
...session.messages,
|
||||
{
|
||||
id: crypto.randomUUID(),
|
||||
role: 'assistant',
|
||||
content: session.currentDraftFlow
|
||||
? `This flow "${session.currentDraftFlow.name}" receives input, classifies urgency, routes urgent cases to a human, and completes the non-urgent path automatically.`
|
||||
: 'There is no draft flow in the session yet.'
|
||||
}
|
||||
];
|
||||
return;
|
||||
}
|
||||
|
||||
let draft = session.currentDraftFlow;
|
||||
let validationErrors: AssistantValidationIssue[] = [];
|
||||
let assistantMessage = 'I updated the workflow based on your request.';
|
||||
|
||||
if (!draft || /create|new flow|from scratch/.test(content)) {
|
||||
draft = {
|
||||
override draft(request: AssistantFlowRequest): Observable<AssistantFlowActionResult> {
|
||||
const model = request.llmSelection?.model ?? this.models[0];
|
||||
return of({
|
||||
flow: {
|
||||
name: 'Ticket classification with urgent review',
|
||||
description: `Draft generated from prompt: ${call.content}`,
|
||||
flow: buildTicketFlow(session.selectedModel)
|
||||
};
|
||||
assistantMessage = 'I created a new workflow draft.';
|
||||
} else if (/review|approval|human/.test(content)) {
|
||||
draft = structuredClone(draft);
|
||||
addHumanReviewTail(draft.flow);
|
||||
assistantMessage = 'I updated the current workflow based on your request.';
|
||||
} else if (/fix|repair|invalid|error|bug/.test(content)) {
|
||||
draft = structuredClone(draft);
|
||||
validationErrors = [];
|
||||
assistantMessage = 'I fixed the current workflow.';
|
||||
}
|
||||
|
||||
if (/invalid|broken/.test(content)) {
|
||||
validationErrors = [{ message: 'The draft contains unresolved validation issues.' }];
|
||||
assistantMessage = 'I created an initial draft, but it still needs corrections.';
|
||||
}
|
||||
|
||||
session.currentFlow = draft;
|
||||
session.currentDraftFlow = draft;
|
||||
session.lastValidationErrors = validationErrors;
|
||||
session.messages = [
|
||||
...session.messages,
|
||||
{
|
||||
id: crypto.randomUUID(),
|
||||
role: 'assistant',
|
||||
content: `${assistantMessage} ${validationErrors.length ? 'The draft still has validation errors.' : 'The draft is valid.'}`.trim(),
|
||||
warnings: ['Fake assistant response'],
|
||||
validationErrors
|
||||
}
|
||||
];
|
||||
description: `Draft generated from prompt: ${request.userPrompt}`,
|
||||
flow: buildTicketFlow(model)
|
||||
},
|
||||
valid: true,
|
||||
validationErrors: [],
|
||||
warnings: ['Fake assistant response'],
|
||||
message: 'I created a new workflow draft.'
|
||||
});
|
||||
}
|
||||
|
||||
override refine(request: AssistantFlowRequest): Observable<AssistantFlowActionResult> {
|
||||
const flow = request.flow ? structuredClone(request.flow) : null;
|
||||
if (flow) addHumanReviewTail(flow.flow);
|
||||
return of({
|
||||
flow,
|
||||
valid: true,
|
||||
validationErrors: [],
|
||||
warnings: ['Fake assistant response'],
|
||||
message: 'I updated the current workflow based on your request.'
|
||||
});
|
||||
}
|
||||
|
||||
override fix(request: AssistantFlowRequest): Observable<AssistantFlowActionResult> {
|
||||
return of({
|
||||
flow: request.flow ? structuredClone(request.flow) : null,
|
||||
valid: true,
|
||||
validationErrors: [],
|
||||
warnings: ['Fake assistant response'],
|
||||
message: 'I fixed the current workflow.'
|
||||
});
|
||||
}
|
||||
|
||||
override explain(_request: AssistantFlowRequest): Observable<AssistantFlowActionResult> {
|
||||
return of({
|
||||
flow: null,
|
||||
validationErrors: [],
|
||||
warnings: [],
|
||||
message: 'This workflow classifies incoming tickets and routes urgent cases to a human reviewer.'
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function buildTicketFlow(model: string): FlowData {
|
||||
|
|
|
|||
|
|
@ -28,13 +28,10 @@ describe('AssistantCallService', () => {
|
|||
});
|
||||
|
||||
it('normalizes node families in assistant drafts and nested container subflows', async () => {
|
||||
const request = firstValueFrom(service.getSession('session-1'));
|
||||
const request = firstValueFrom(service.draft({ userPrompt: 'Create a flow' }));
|
||||
|
||||
httpMock.expectOne(`${environment.apiUrl}/assistant/sessions/session-1`).flush({
|
||||
id: 'session-1',
|
||||
selectedModel: 'model-1',
|
||||
messages: [],
|
||||
currentDraftFlow: {
|
||||
httpMock.expectOne(`${environment.apiUrl}/assistant/flows/draft`).flush({
|
||||
flow: {
|
||||
name: 'Loop draft',
|
||||
flow: {
|
||||
blocks: [{ id: 'root-block', typeName: 'LLMBlock', specificConfiguration: {} }],
|
||||
|
|
@ -68,8 +65,8 @@ describe('AssistantCallService', () => {
|
|||
}
|
||||
});
|
||||
|
||||
const session = await request;
|
||||
const flow = session.currentDraftFlow!.flow;
|
||||
const result = await request;
|
||||
const flow = result.flow!.flow;
|
||||
const loopConfiguration = flow.containers[0].specificConfiguration as Record<string, any>;
|
||||
|
||||
expect(flow.blocks[0].nodeFamily).toBe('block');
|
||||
|
|
@ -86,4 +83,46 @@ describe('AssistantCallService', () => {
|
|||
});
|
||||
expect(loopConfiguration['guardSubFlow'].blocks[0].nodeFamily).toBe('block');
|
||||
});
|
||||
|
||||
it('loads providers and provider-specific models from the configured retrievers', async () => {
|
||||
const providers = firstValueFrom(service.listProviders('/retriever/LLM/providers'));
|
||||
httpMock.expectOne(`${environment.apiUrl}/retriever/LLM/providers`).flush(['InternalOllama', 'OpenAI']);
|
||||
await expect(providers).resolves.toEqual(['InternalOllama', 'OpenAI']);
|
||||
|
||||
const models = firstValueFrom(service.listModels('/retriever/LLM/models?provider={provider}', 'Open AI'));
|
||||
httpMock.expectOne(`${environment.apiUrl}/retriever/LLM/models?provider=Open%20AI`).flush(['gpt-oss:20b']);
|
||||
await expect(models).resolves.toEqual(['gpt-oss:20b']);
|
||||
});
|
||||
|
||||
it('sends llmSelection only when supplied, for sessions and flow actions', async () => {
|
||||
const defaultSession = firstValueFrom(service.createSession({}));
|
||||
const defaultSessionRequest = httpMock.expectOne(`${environment.apiUrl}/assistant/sessions`);
|
||||
expect(defaultSessionRequest.request.body).toEqual({});
|
||||
defaultSessionRequest.flush({ id: 'session-default', messages: [] });
|
||||
await defaultSession;
|
||||
|
||||
const selection = {
|
||||
provider: 'OpenAI',
|
||||
model: 'gpt-oss:20b',
|
||||
phaseModels: { planningModel: 'planning-model' }
|
||||
};
|
||||
const actionRequests = [
|
||||
['draft', service.draft({ userPrompt: 'Create a flow', llmSelection: selection })],
|
||||
['refine', service.refine({ userPrompt: 'Refine it', flow: { name: 'Flow', flow: emptyFlow() }, llmSelection: selection })],
|
||||
['fix', service.fix({ userPrompt: 'Fix it', flow: { name: 'Flow', flow: emptyFlow() }, llmSelection: selection })],
|
||||
['explain', service.explain({ userPrompt: 'Explain it', flow: { name: 'Flow', flow: emptyFlow() }, llmSelection: selection })]
|
||||
] as const;
|
||||
|
||||
for (const [action, observable] of actionRequests) {
|
||||
const result = firstValueFrom(observable);
|
||||
const request = httpMock.expectOne(`${environment.apiUrl}/assistant/flows/${action}`);
|
||||
expect(request.request.body.llmSelection).toEqual(selection);
|
||||
request.flush({ message: 'Done' });
|
||||
await result;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
function emptyFlow() {
|
||||
return { blocks: [], containers: [], connections: [], dependencies: [], globalInputs: [], lanes: [] };
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,15 +1,12 @@
|
|||
import { HttpClient } from '@angular/common/http';
|
||||
import { inject } from '@angular/core';
|
||||
import {
|
||||
AssistantCallPhase,
|
||||
AssistantCallState,
|
||||
AssistantCallStatus,
|
||||
AssistantChatMessage,
|
||||
AssistantConfig,
|
||||
AssistantDraftPayload,
|
||||
AssistantFlowResult,
|
||||
AssistantIntent,
|
||||
AssistantSendMessageRequest,
|
||||
AssistantFlowActionResult,
|
||||
AssistantFlowRequest,
|
||||
AssistantSessionRequest,
|
||||
AssistantSessionState,
|
||||
AssistantValidationIssue
|
||||
} from '@models/assistant';
|
||||
|
|
@ -26,56 +23,69 @@ export class AssistantCallService extends AssistantCallServiceBase {
|
|||
.pipe(map((raw) => mapAssistantConfig(raw)));
|
||||
}
|
||||
|
||||
override listModels(retrieverUrl: string): Observable<string[]> {
|
||||
override listProviders(retrieverUrl: string): Observable<string[]> {
|
||||
return this.http
|
||||
.get<unknown>(resolveAssistantUrl(retrieverUrl))
|
||||
.pipe(map((raw) => mapModelList(raw)));
|
||||
}
|
||||
|
||||
override listModels(retrieverUrlTemplate: string, provider: string): Observable<string[]> {
|
||||
const retrieverUrl = retrieverUrlTemplate.replace('{provider}', encodeURIComponent(provider));
|
||||
const resolvedUrl = resolveAssistantUrl(retrieverUrl);
|
||||
return this.http
|
||||
.get<unknown>(resolvedUrl)
|
||||
.pipe(map((raw) => mapModelList(raw)));
|
||||
}
|
||||
|
||||
override createSession(request: {
|
||||
model: string;
|
||||
}): Observable<AssistantSessionState> {
|
||||
override createSession(request: AssistantSessionRequest): Observable<AssistantSessionState> {
|
||||
return this.http
|
||||
.post<unknown>(`${environment.apiUrl}/assistant/sessions`, request)
|
||||
.pipe(map((raw) => mapAssistantSessionState(raw)));
|
||||
}
|
||||
|
||||
override sendMessage(sessionId: string, request: AssistantSendMessageRequest): Observable<{ callId: string }> {
|
||||
const encodedId = encodeURIComponent(sessionId);
|
||||
return this.http
|
||||
.post<unknown>(`${environment.apiUrl}/assistant/sessions/${encodedId}/messages`, request)
|
||||
.pipe(map((raw) => mapSendMessageResponse(raw)));
|
||||
override draft(request: AssistantFlowRequest): Observable<AssistantFlowActionResult> {
|
||||
return this.runFlowAction('draft', request);
|
||||
}
|
||||
|
||||
override getCall(callId: string): Observable<AssistantCallState> {
|
||||
const encodedId = encodeURIComponent(callId);
|
||||
return this.http
|
||||
.get<unknown>(`${environment.apiUrl}/assistant/calls/${encodedId}`)
|
||||
.pipe(map((raw) => mapAssistantCallState(raw)));
|
||||
override refine(request: AssistantFlowRequest): Observable<AssistantFlowActionResult> {
|
||||
return this.runFlowAction('refine', request);
|
||||
}
|
||||
|
||||
override cancelCall(callId: string): Observable<AssistantCallState> {
|
||||
const encodedId = encodeURIComponent(callId);
|
||||
return this.http
|
||||
.put<unknown>(`${environment.apiUrl}/assistant/calls/${encodedId}/cancel`, {})
|
||||
.pipe(map((raw) => mapAssistantCallState(raw)));
|
||||
override fix(request: AssistantFlowRequest): Observable<AssistantFlowActionResult> {
|
||||
return this.runFlowAction('fix', request);
|
||||
}
|
||||
|
||||
override getSession(sessionId: string): Observable<AssistantSessionState> {
|
||||
const encodedId = encodeURIComponent(sessionId);
|
||||
override explain(request: AssistantFlowRequest): Observable<AssistantFlowActionResult> {
|
||||
return this.runFlowAction('explain', request);
|
||||
}
|
||||
|
||||
private runFlowAction(
|
||||
action: 'draft' | 'refine' | 'fix' | 'explain',
|
||||
request: AssistantFlowRequest
|
||||
): Observable<AssistantFlowActionResult> {
|
||||
return this.http
|
||||
.get<unknown>(`${environment.apiUrl}/assistant/sessions/${encodedId}`)
|
||||
.pipe(map((raw) => mapAssistantSessionState(raw)));
|
||||
.post<unknown>(`${environment.apiUrl}/assistant/flows/${action}`, request)
|
||||
.pipe(map((raw) => mapAssistantFlowActionResult(raw)));
|
||||
}
|
||||
}
|
||||
|
||||
function mapAssistantConfig(raw: unknown): AssistantConfig {
|
||||
const value = (raw ?? {}) as Record<string, unknown>;
|
||||
return {
|
||||
provider: typeof value['provider'] === 'string' ? value['provider'] : undefined,
|
||||
defaultProvider: String(value['defaultProvider'] ?? value['provider'] ?? ''),
|
||||
defaultModel: String(value['defaultModel'] ?? ''),
|
||||
availableModelsRetrieverUrl: String(value['availableModelsRetrieverUrl'] ?? '')
|
||||
availableProvidersRetrieverUrl: String(value['availableProvidersRetrieverUrl'] ?? ''),
|
||||
availableModelsRetrieverUrl: String(value['availableModelsRetrieverUrlTemplate'] ?? value['availableModelsRetrieverUrl'] ?? ''),
|
||||
defaultPhaseModels: mapPhaseModels(value['defaultPhaseModels'])
|
||||
};
|
||||
}
|
||||
|
||||
function mapPhaseModels(raw: unknown) {
|
||||
const value = (raw && typeof raw === 'object' ? raw : {}) as Record<string, unknown>;
|
||||
return {
|
||||
planningModel: typeof value['planningModel'] === 'string' ? value['planningModel'] : undefined,
|
||||
jsonModel: typeof value['jsonModel'] === 'string' ? value['jsonModel'] : undefined,
|
||||
repairModel: typeof value['repairModel'] === 'string' ? value['repairModel'] : undefined
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -99,29 +109,25 @@ function mapModelList(raw: unknown): string[] {
|
|||
}).filter((item) => item.length > 0);
|
||||
}
|
||||
|
||||
function mapSendMessageResponse(raw: unknown): { callId: string } {
|
||||
const value = (raw ?? {}) as Record<string, unknown>;
|
||||
return {
|
||||
callId: String(value['callId'] ?? value['id'] ?? '')
|
||||
};
|
||||
}
|
||||
|
||||
function mapAssistantCallState(raw: unknown): AssistantCallState {
|
||||
const value = (raw ?? {}) as Record<string, unknown>;
|
||||
const status = mapCallStatus(value['status']);
|
||||
return {
|
||||
id: String(value['id'] ?? value['callId'] ?? ''),
|
||||
sessionId: String(value['sessionId'] ?? ''),
|
||||
status,
|
||||
phase: mapCallPhase(value['phase']),
|
||||
progressMessage: typeof value['progressMessage'] === 'string' ? value['progressMessage'] : undefined,
|
||||
intent: mapIntent(value['intent']),
|
||||
errorMessage: typeof value['errorMessage'] === 'string'
|
||||
? value['errorMessage']
|
||||
: typeof value['message'] === 'string' && status === 'FAILED'
|
||||
function mapAssistantFlowActionResult(raw: unknown): AssistantFlowActionResult {
|
||||
const value = (raw && typeof raw === 'object' ? raw : {}) as Record<string, unknown>;
|
||||
const flow = mapAssistantDraftPayload(value['flow'] ?? value['result']);
|
||||
const message = typeof value['assistantRationale'] === 'string'
|
||||
? value['assistantRationale']
|
||||
: typeof value['explanation'] === 'string'
|
||||
? value['explanation']
|
||||
: typeof value['message'] === 'string'
|
||||
? value['message']
|
||||
: undefined,
|
||||
flowResult: mapAssistantFlowResult(value['flowResult'])
|
||||
: flow
|
||||
? 'Workflow updated.'
|
||||
: 'The assistant completed the request.';
|
||||
|
||||
return {
|
||||
flow,
|
||||
valid: typeof value['valid'] === 'boolean' ? value['valid'] : undefined,
|
||||
validationErrors: mapValidationIssues(value['validationErrors']),
|
||||
warnings: Array.isArray(value['warnings']) ? value['warnings'].map((warning) => String(warning)) : [],
|
||||
message
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -159,18 +165,6 @@ function mapAssistantMessages(raw: unknown): AssistantChatMessage[] {
|
|||
});
|
||||
}
|
||||
|
||||
function mapAssistantFlowResult(raw: unknown): AssistantFlowResult | null {
|
||||
if (!raw || typeof raw !== 'object') return null;
|
||||
const value = raw as Record<string, unknown>;
|
||||
const flow = mapAssistantFlowData(value['flow']);
|
||||
if (!hasAssistantFlowData(flow)) return null;
|
||||
return {
|
||||
name: typeof value['name'] === 'string' ? value['name'] : undefined,
|
||||
description: typeof value['description'] === 'string' ? value['description'] : undefined,
|
||||
flow
|
||||
};
|
||||
}
|
||||
|
||||
function mapAssistantDraftPayload(raw: unknown): AssistantDraftPayload | null {
|
||||
if (!raw || typeof raw !== 'object') return null;
|
||||
|
||||
|
|
@ -276,45 +270,6 @@ function mapValidationIssues(raw: unknown): AssistantValidationIssue[] {
|
|||
});
|
||||
}
|
||||
|
||||
function mapCallStatus(raw: unknown): AssistantCallStatus {
|
||||
const normalized = typeof raw === 'string' ? raw.toUpperCase() : '';
|
||||
if (normalized === 'QUEUED' || normalized === 'RUNNING' || normalized === 'FAILED' || normalized === 'CANCELLED') return normalized;
|
||||
return 'COMPLETED';
|
||||
}
|
||||
|
||||
function mapCallPhase(raw: unknown): AssistantCallPhase {
|
||||
const normalized = typeof raw === 'string' ? raw.toLowerCase() : '';
|
||||
switch (normalized) {
|
||||
case 'queued':
|
||||
case 'routing':
|
||||
case 'planning':
|
||||
case 'configuring_blocks':
|
||||
case 'connecting_blocks':
|
||||
case 'validating':
|
||||
case 'fixing':
|
||||
case 'explaining':
|
||||
case 'completed':
|
||||
case 'failed':
|
||||
case 'cancelled':
|
||||
return normalized;
|
||||
default:
|
||||
return 'queued';
|
||||
}
|
||||
}
|
||||
|
||||
function mapIntent(raw: unknown): AssistantIntent | null {
|
||||
const normalized = typeof raw === 'string' ? raw.toLowerCase() : '';
|
||||
switch (normalized) {
|
||||
case 'draft':
|
||||
case 'refine':
|
||||
case 'fix':
|
||||
case 'explain':
|
||||
return normalized;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function resolveAssistantUrl(url: string): string {
|
||||
if (!url) return url;
|
||||
if (/^https?:\/\//i.test(url)) return url;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,9 @@
|
|||
import { Injectable } from '@angular/core';
|
||||
import { environment } from '@environment';
|
||||
import { AssistantSendMessageRequest } from '@models/assistant';
|
||||
import {
|
||||
AssistantFlowRequest,
|
||||
AssistantSessionRequest
|
||||
} from '@models/assistant';
|
||||
import { AssistantCallServiceBase } from './assistant-call.base';
|
||||
|
||||
@Injectable({
|
||||
|
|
@ -13,27 +16,32 @@ export class AssistantService {
|
|||
return this.assistantCall.getConfig();
|
||||
}
|
||||
|
||||
listModels(retrieverUrl: string) {
|
||||
return this.assistantCall.listModels(retrieverUrl);
|
||||
listProviders(retrieverUrl: string) {
|
||||
return this.assistantCall.listProviders(retrieverUrl);
|
||||
}
|
||||
|
||||
createSession(request: { model: string }) {
|
||||
listModels(retrieverUrlTemplate: string, provider: string) {
|
||||
return this.assistantCall.listModels(retrieverUrlTemplate, provider);
|
||||
}
|
||||
|
||||
createSession(request: AssistantSessionRequest) {
|
||||
return this.assistantCall.createSession(request);
|
||||
}
|
||||
|
||||
sendMessage(sessionId: string, request: AssistantSendMessageRequest) {
|
||||
return this.assistantCall.sendMessage(sessionId, request);
|
||||
draft(request: AssistantFlowRequest) {
|
||||
return this.assistantCall.draft(request);
|
||||
}
|
||||
|
||||
getCall(callId: string) {
|
||||
return this.assistantCall.getCall(callId);
|
||||
refine(request: AssistantFlowRequest) {
|
||||
return this.assistantCall.refine(request);
|
||||
}
|
||||
|
||||
cancelCall(callId: string) {
|
||||
return this.assistantCall.cancelCall(callId);
|
||||
fix(request: AssistantFlowRequest) {
|
||||
return this.assistantCall.fix(request);
|
||||
}
|
||||
|
||||
getSession(sessionId: string) {
|
||||
return this.assistantCall.getSession(sessionId);
|
||||
explain(request: AssistantFlowRequest) {
|
||||
return this.assistantCall.explain(request);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -232,6 +232,42 @@
|
|||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.assistant-llm-settings {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.assistant-llm-settings .assistant-copy {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.assistant-settings-fields {
|
||||
display: grid;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.assistant-phase-fields {
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.assistant-advanced-toggle,
|
||||
.assistant-new-session {
|
||||
justify-self: start;
|
||||
border: 0;
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
color: #0f766e;
|
||||
font: inherit;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.assistant-advanced-toggle:disabled {
|
||||
color: #94a3b8;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.assistant-label {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
|
|
@ -441,3 +477,9 @@
|
|||
border-top: 1px solid #dbe4ee;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 520px) {
|
||||
.assistant-phase-fields {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -39,6 +39,99 @@
|
|||
}
|
||||
|
||||
@if (!progressOnlyMode()) {
|
||||
<section class="assistant-card assistant-llm-settings">
|
||||
<div class="assistant-starters-head">
|
||||
<div>
|
||||
<p class="assistant-label">Configurazione assistente</p>
|
||||
<p class="assistant-copy">Le credenziali dei provider sono gestite dal backend.</p>
|
||||
</div>
|
||||
@if (configurationLocked()) {
|
||||
<button type="button" mat-stroked-button class="assistant-new-session" [disabled]="assistantBusy()" (click)="createNewSession()">
|
||||
Nuova sessione
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
|
||||
<mat-checkbox
|
||||
[checked]="useDefaultConfiguration()"
|
||||
[disabled]="configurationLocked() || assistantBusy()"
|
||||
(change)="setUseDefaultConfiguration($event.checked)">
|
||||
Usa configurazione predefinita
|
||||
</mat-checkbox>
|
||||
|
||||
@if (configurationLocked()) {
|
||||
<p class="assistant-meta">La configurazione LLM è fissata per questa sessione. Crea una nuova sessione per modificarla.</p>
|
||||
}
|
||||
|
||||
@if (!useDefaultConfiguration()) {
|
||||
<div class="assistant-settings-fields">
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Provider</mat-label>
|
||||
<mat-select
|
||||
[value]="selectedProvider()"
|
||||
[disabled]="configurationLocked() || assistantBusy() || providersLoading()"
|
||||
(selectionChange)="selectProvider($event.value)">
|
||||
@for (provider of providers(); track provider) {
|
||||
<mat-option [value]="provider">{{ provider }}</mat-option>
|
||||
}
|
||||
</mat-select>
|
||||
@if (providersLoading()) { <mat-hint>Caricamento provider...</mat-hint> }
|
||||
@if (providersError()) { <mat-error>{{ providersError() }}</mat-error> }
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Modello</mat-label>
|
||||
<mat-select
|
||||
[value]="selectedModel()"
|
||||
[disabled]="configurationLocked() || assistantBusy() || !selectedProvider() || modelsLoading()"
|
||||
(selectionChange)="selectModel($event.value)">
|
||||
@for (model of models(); track model) {
|
||||
<mat-option [value]="model">{{ model }}</mat-option>
|
||||
}
|
||||
</mat-select>
|
||||
@if (modelsLoading()) { <mat-hint>Caricamento modelli...</mat-hint> }
|
||||
@if (modelsError()) { <mat-error>{{ modelsError() }}</mat-error> }
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<button type="button" class="assistant-advanced-toggle" [disabled]="configurationLocked() || assistantBusy()" (click)="toggleAdvancedModels()">
|
||||
{{ advancedModelsOpen() ? 'Nascondi' : 'Mostra' }} modelli per fase (opzionale)
|
||||
</button>
|
||||
|
||||
@if (advancedModelsOpen()) {
|
||||
<div class="assistant-settings-fields assistant-phase-fields">
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Planning</mat-label>
|
||||
<mat-select [value]="phaseModels()?.planningModel ?? ''" [disabled]="configurationLocked() || assistantBusy() || !models().length" (selectionChange)="setPhaseModel('planningModel', $event.value)">
|
||||
<mat-option value="">Modello principale</mat-option>
|
||||
@for (model of models(); track model) { <mat-option [value]="model">{{ model }}</mat-option> }
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>JSON</mat-label>
|
||||
<mat-select [value]="phaseModels()?.jsonModel ?? ''" [disabled]="configurationLocked() || assistantBusy() || !models().length" (selectionChange)="setPhaseModel('jsonModel', $event.value)">
|
||||
<mat-option value="">Modello principale</mat-option>
|
||||
@for (model of models(); track model) { <mat-option [value]="model">{{ model }}</mat-option> }
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Repair</mat-label>
|
||||
<mat-select [value]="phaseModels()?.repairModel ?? ''" [disabled]="configurationLocked() || assistantBusy() || !models().length" (selectionChange)="setPhaseModel('repairModel', $event.value)">
|
||||
<mat-option value="">Modello principale</mat-option>
|
||||
@for (model of models(); track model) { <mat-option [value]="model">{{ model }}</mat-option> }
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</section>
|
||||
|
||||
@if (assistantErrorMessage()) {
|
||||
<section class="assistant-card assistant-error-card">
|
||||
<p class="assistant-error-message">{{ assistantErrorMessage() }}</p>
|
||||
</section>
|
||||
}
|
||||
|
||||
<section class="assistant-card assistant-starters">
|
||||
<div class="assistant-starters-head">
|
||||
<p class="assistant-label">Quick prompts</p>
|
||||
|
|
@ -115,7 +208,7 @@
|
|||
class="assistant-input"
|
||||
[ngModel]="prompt()"
|
||||
(ngModelChange)="prompt.set($event)"
|
||||
[disabled]="!selectedModel() || assistantBusy()"
|
||||
[disabled]="!configurationValid() || assistantBusy()"
|
||||
name="assistantPrompt"
|
||||
rows="4"
|
||||
[placeholder]="promptPlaceholder()"></textarea>
|
||||
|
|
@ -125,7 +218,7 @@
|
|||
type="submit"
|
||||
mat-flat-button
|
||||
class="assistant-send"
|
||||
[disabled]="!selectedModel() || assistantBusy() || !prompt().trim()">
|
||||
[disabled]="!configurationValid() || assistantBusy() || !prompt().trim()">
|
||||
Send
|
||||
</button>
|
||||
</form>
|
||||
|
|
|
|||
|
|
@ -1,18 +1,19 @@
|
|||
import { CommonModule } from '@angular/common';
|
||||
import { ChangeDetectionStrategy, Component, computed, effect, ElementRef, EventEmitter, inject, input, OnDestroy, OnInit, Output, signal, ViewChild } from '@angular/core';
|
||||
import { MatCheckboxModule } from '@angular/material/checkbox';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { MatSelectModule } from '@angular/material/select';
|
||||
import { environment } from '@environment';
|
||||
import {
|
||||
AssistantCallPhase,
|
||||
AssistantCallState,
|
||||
AssistantChatMessage,
|
||||
AssistantConfig,
|
||||
AssistantDraftPayload,
|
||||
AssistantFlowResult,
|
||||
AssistantFlowActionResult,
|
||||
AssistantLlmSelection,
|
||||
AssistantSessionState
|
||||
} from '@models/assistant';
|
||||
import { Flow } from '@models/flow';
|
||||
|
|
@ -24,7 +25,7 @@ import { finalize, firstValueFrom, interval, Subscription, switchMap, take } fro
|
|||
|
||||
@Component({
|
||||
selector: 'app-flow-assistant',
|
||||
imports: [CommonModule, FormsModule, MatButtonModule, MatFormFieldModule, MatInputModule, MatSelectModule],
|
||||
imports: [CommonModule, FormsModule, MatButtonModule, MatCheckboxModule, MatFormFieldModule, MatInputModule, MatSelectModule],
|
||||
templateUrl: './flow-assistant.html',
|
||||
styleUrl: './flow-assistant.css',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
|
|
@ -52,6 +53,9 @@ export class FlowAssistant implements OnInit, OnDestroy {
|
|||
readonly currentCall = signal<AssistantCallState | null>(null);
|
||||
readonly localMessages = signal<AssistantChatMessage[]>([]);
|
||||
readonly models = signal<string[]>([]);
|
||||
readonly providers = signal<string[]>([]);
|
||||
readonly providersLoading = signal(false);
|
||||
readonly providersError = signal<string | null>(null);
|
||||
readonly modelsLoading = signal(false);
|
||||
readonly modelsError = signal<string | null>(null);
|
||||
readonly sessionLoading = signal(false);
|
||||
|
|
@ -59,6 +63,10 @@ export class FlowAssistant implements OnInit, OnDestroy {
|
|||
readonly prompt = signal('');
|
||||
readonly createPromptSubmitted = signal(false);
|
||||
readonly selectedModel = signal('');
|
||||
readonly useDefaultConfiguration = signal(true);
|
||||
readonly selectedProvider = signal('');
|
||||
readonly phaseModels = signal<AssistantLlmSelection['phaseModels']>(undefined);
|
||||
readonly advancedModelsOpen = signal(false);
|
||||
readonly modelPickerOpen = signal(false);
|
||||
readonly quickPromptsOpen = signal(true);
|
||||
readonly assistantErrorMessage = signal<string | null>(null);
|
||||
|
|
@ -180,6 +188,13 @@ export class FlowAssistant implements OnInit, OnDestroy {
|
|||
});
|
||||
readonly currentFlow = this.editorState.currentFlow;
|
||||
readonly currentDraft = computed(() => this.sessionState()?.currentDraftFlow ?? null);
|
||||
readonly configurationLocked = computed(() => !!this.sessionState()?.id);
|
||||
readonly customConfigurationValid = computed(() =>
|
||||
!!this.selectedProvider().trim() && !!this.selectedModel().trim()
|
||||
);
|
||||
readonly configurationValid = computed(() =>
|
||||
this.useDefaultConfiguration() || this.customConfigurationValid()
|
||||
);
|
||||
|
||||
constructor() {
|
||||
effect(() => {
|
||||
|
|
@ -226,9 +241,52 @@ export class FlowAssistant implements OnInit, OnDestroy {
|
|||
}
|
||||
|
||||
selectModel(model: string) {
|
||||
if (!model || model === this.selectedModel()) return;
|
||||
if (this.configurationLocked()) return;
|
||||
this.selectedModel.set(model);
|
||||
void this.openSession(model);
|
||||
this.persistSnapshot();
|
||||
}
|
||||
|
||||
setUseDefaultConfiguration(useDefault: boolean) {
|
||||
if (this.configurationLocked()) return;
|
||||
this.useDefaultConfiguration.set(useDefault);
|
||||
this.modelsError.set(null);
|
||||
if (!useDefault) {
|
||||
void this.loadProviders();
|
||||
}
|
||||
this.persistSnapshot();
|
||||
}
|
||||
|
||||
selectProvider(provider: string) {
|
||||
if (this.configurationLocked()) return;
|
||||
this.selectedProvider.set(provider);
|
||||
this.selectedModel.set('');
|
||||
this.models.set([]);
|
||||
this.modelsError.set(null);
|
||||
if (provider) void this.loadModels(provider);
|
||||
this.persistSnapshot();
|
||||
}
|
||||
|
||||
setPhaseModel(phase: keyof NonNullable<AssistantLlmSelection['phaseModels']>, model: string) {
|
||||
if (this.configurationLocked()) return;
|
||||
this.phaseModels.update((current) => ({ ...current, [phase]: model || undefined }));
|
||||
this.persistSnapshot();
|
||||
}
|
||||
|
||||
toggleAdvancedModels() {
|
||||
this.advancedModelsOpen.update((value) => !value);
|
||||
this.persistSnapshot();
|
||||
}
|
||||
|
||||
createNewSession() {
|
||||
if (this.assistantBusy() || !this.configurationValid()) return;
|
||||
this.stopPolling();
|
||||
this.currentCall.set(null);
|
||||
this.sessionState.set(null);
|
||||
this.localMessages.set([]);
|
||||
this.assistantErrorMessage.set(null);
|
||||
this.lastFailedPrompt.set(null);
|
||||
this.lastSubmittedPrompt.set('');
|
||||
void this.openSession();
|
||||
}
|
||||
|
||||
useStarter(prompt: string) {
|
||||
|
|
@ -264,40 +322,12 @@ export class FlowAssistant implements OnInit, OnDestroy {
|
|||
=== this.canonicalAssistantErrorContent(FlowAssistant.STANDARD_ASSISTANT_ERROR);
|
||||
}
|
||||
|
||||
hasCancellableCall(): boolean {
|
||||
return this.isActiveCall(this.currentCall());
|
||||
}
|
||||
hasCancellableCall(): boolean { return false; }
|
||||
|
||||
async cancelActiveCall(): Promise<boolean> {
|
||||
const call = this.currentCall();
|
||||
if (!this.isActiveCall(call)) {
|
||||
this.requestPending.set(false);
|
||||
this.createPromptSubmitted.set(false);
|
||||
return true;
|
||||
}
|
||||
|
||||
this.stopPolling();
|
||||
this.requestPending.set(false);
|
||||
|
||||
try {
|
||||
const cancelledCall = await firstValueFrom(this.assistant.cancelCall(call.id).pipe(take(1)));
|
||||
this.currentCall.set(cancelledCall);
|
||||
this.createPromptSubmitted.set(false);
|
||||
this.persistSnapshot();
|
||||
|
||||
const session = await firstValueFrom(this.assistant.getSession(cancelledCall.sessionId || call.sessionId).pipe(take(1)));
|
||||
this.applySessionState(session, { syncDraftToEditor: false });
|
||||
this.currentCall.set(null);
|
||||
this.persistSnapshot();
|
||||
return true;
|
||||
} catch (err) {
|
||||
console.error('Assistant call cancel failed', err);
|
||||
this.currentCall.set(call);
|
||||
this.startPolling(call.id, call.sessionId);
|
||||
this.assistantErrorMessage.set('Unable to cancel the assistant request.');
|
||||
this.persistSnapshot();
|
||||
return false;
|
||||
}
|
||||
this.createPromptSubmitted.set(false);
|
||||
return true;
|
||||
}
|
||||
|
||||
clearActiveSnapshot() {
|
||||
|
|
@ -307,8 +337,18 @@ export class FlowAssistant implements OnInit, OnDestroy {
|
|||
|
||||
private sendPrompt(content: string) {
|
||||
const normalizedContent = content.trim();
|
||||
const sessionId = this.sessionState()?.id;
|
||||
if (!normalizedContent || this.assistantBusy() || !sessionId) return;
|
||||
if (!normalizedContent || this.assistantBusy() || !this.configurationValid()) return;
|
||||
|
||||
if (!this.sessionState()?.id) {
|
||||
void this.openSession(normalizedContent);
|
||||
return;
|
||||
}
|
||||
|
||||
this.runFlowAction(normalizedContent);
|
||||
}
|
||||
|
||||
private runFlowAction(normalizedContent: string) {
|
||||
const intent = this.resolveIntent(normalizedContent);
|
||||
|
||||
if (this.isCreateModal()) this.createPromptSubmitted.set(true);
|
||||
this.requestPending.set(true);
|
||||
|
|
@ -324,24 +364,31 @@ export class FlowAssistant implements OnInit, OnDestroy {
|
|||
]);
|
||||
this.persistSnapshot();
|
||||
|
||||
this.assistant.sendMessage(sessionId, { message: normalizedContent }).pipe(
|
||||
take(1)
|
||||
).subscribe({
|
||||
next: ({ callId }) => {
|
||||
const request = {
|
||||
userPrompt: normalizedContent,
|
||||
...(this.llmSelection() ? { llmSelection: this.llmSelection() } : {}),
|
||||
...(intent === 'draft' ? {} : { flow: this.assistantFlowForRequest() }),
|
||||
...(intent === 'fix' ? { validationErrors: this.sessionState()?.lastValidationErrors ?? [] } : {})
|
||||
};
|
||||
const action = intent === 'draft'
|
||||
? this.assistant.draft(request)
|
||||
: intent === 'fix'
|
||||
? this.assistant.fix(request)
|
||||
: intent === 'explain'
|
||||
? this.assistant.explain(request)
|
||||
: this.assistant.refine(request);
|
||||
|
||||
action.pipe(take(1)).subscribe({
|
||||
next: (result) => {
|
||||
this.requestPending.set(false);
|
||||
this.currentCall.set({
|
||||
id: callId,
|
||||
sessionId,
|
||||
status: 'QUEUED',
|
||||
phase: 'queued'
|
||||
});
|
||||
this.applyFlowActionResult(result);
|
||||
this.createPromptSubmitted.set(false);
|
||||
this.persistSnapshot();
|
||||
this.startPolling(callId, sessionId);
|
||||
},
|
||||
error: (err) => {
|
||||
console.error('Assistant send message failed', err);
|
||||
console.error('Assistant flow action failed', err);
|
||||
this.requestPending.set(false);
|
||||
this.handleAssistantErrorWithRetry(normalizedContent);
|
||||
this.handleAssistantErrorWithRetry(normalizedContent, this.backendErrorMessage(err));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -354,7 +401,7 @@ export class FlowAssistant implements OnInit, OnDestroy {
|
|||
).subscribe({
|
||||
next: (config) => {
|
||||
this.assistantConfig.set(config);
|
||||
this.loadModelsAndSession(config);
|
||||
this.initializeConfiguration(config);
|
||||
},
|
||||
error: (err) => {
|
||||
console.error('Assistant config loading failed', err);
|
||||
|
|
@ -364,112 +411,150 @@ export class FlowAssistant implements OnInit, OnDestroy {
|
|||
});
|
||||
}
|
||||
|
||||
private loadModelsAndSession(config: AssistantConfig) {
|
||||
private initializeConfiguration(_config: AssistantConfig) {
|
||||
this.activeFlowKey = this.isCreateModal()
|
||||
? this.createModalFlowKey
|
||||
: this.resolveFlowKey(this.currentFlow()?.id ?? null);
|
||||
this.initialized = true;
|
||||
void this.restoreSessionForFlow(this.activeFlowKey);
|
||||
}
|
||||
|
||||
private loadProviders() {
|
||||
const config = this.assistantConfig();
|
||||
if (!config?.availableProvidersRetrieverUrl) return;
|
||||
this.providersLoading.set(true);
|
||||
this.providersError.set(null);
|
||||
this.assistant.listProviders(config.availableProvidersRetrieverUrl).pipe(
|
||||
take(1),
|
||||
finalize(() => this.providersLoading.set(false))
|
||||
).subscribe({
|
||||
next: (providers) => this.providers.set(providers),
|
||||
error: (err) => this.providersError.set(this.backendErrorMessage(err))
|
||||
});
|
||||
}
|
||||
|
||||
private loadModels(provider: string) {
|
||||
const config = this.assistantConfig();
|
||||
if (!config?.availableModelsRetrieverUrl) return;
|
||||
this.modelsLoading.set(true);
|
||||
const resolvedUrl = this.resolveAssistantModelsUrl(config.availableModelsRetrieverUrl);
|
||||
this.assistant.listModels(config.availableModelsRetrieverUrl).pipe(
|
||||
this.modelsError.set(null);
|
||||
this.assistant.listModels(config.availableModelsRetrieverUrl, provider).pipe(
|
||||
take(1),
|
||||
finalize(() => this.modelsLoading.set(false))
|
||||
).subscribe({
|
||||
next: (models) => {
|
||||
this.models.set(models);
|
||||
const selectedModel = config.defaultModel || models[0] || '';
|
||||
this.selectedModel.set(selectedModel);
|
||||
if (!selectedModel) {
|
||||
this.sessionLoading.set(false);
|
||||
this.modelsError.set('No assistant model is available.');
|
||||
return;
|
||||
}
|
||||
this.activeFlowKey = this.isCreateModal()
|
||||
? this.createModalFlowKey
|
||||
: this.resolveFlowKey(this.currentFlow()?.id ?? null);
|
||||
this.initialized = true;
|
||||
void this.restoreSessionForFlow(this.activeFlowKey);
|
||||
if (!models.length) this.modelsError.set('No models are available for the selected provider.');
|
||||
},
|
||||
error: (err) => {
|
||||
console.error('Assistant model loading failed', err);
|
||||
this.sessionLoading.set(false);
|
||||
this.modelsError.set(
|
||||
`Unable to load assistant models from ${resolvedUrl}.`
|
||||
);
|
||||
}
|
||||
error: (err) => this.modelsError.set(this.backendErrorMessage(err))
|
||||
});
|
||||
}
|
||||
|
||||
private async openSession(model: string, flowKey = this.activeFlowKey ?? this.resolveFlowKey(this.currentFlow()?.id ?? null)) {
|
||||
private llmSelection(): AssistantLlmSelection | undefined {
|
||||
if (this.useDefaultConfiguration()) return undefined;
|
||||
const provider = this.selectedProvider().trim();
|
||||
const model = this.selectedModel().trim();
|
||||
if (!provider || !model) return undefined;
|
||||
const phaseModels = this.phaseModels();
|
||||
const populatedPhases = phaseModels && Object.values(phaseModels).some(Boolean)
|
||||
? phaseModels
|
||||
: undefined;
|
||||
return { provider, model, ...(populatedPhases ? { phaseModels: populatedPhases } : {}) };
|
||||
}
|
||||
|
||||
private sessionRequest() {
|
||||
const llmSelection = this.llmSelection();
|
||||
return llmSelection ? { llmSelection } : {};
|
||||
}
|
||||
|
||||
private resolveIntent(prompt: string): 'draft' | 'refine' | 'fix' | 'explain' {
|
||||
const normalized = prompt.toLowerCase();
|
||||
if (/\b(explain|what does|why|describe)\b/.test(normalized)) return 'explain';
|
||||
if (/\b(fix|repair|invalid|error|broken)\b/.test(normalized)) return 'fix';
|
||||
return this.canOfferCreate() ? 'draft' : 'refine';
|
||||
}
|
||||
|
||||
private assistantFlowForRequest(): AssistantDraftPayload | undefined {
|
||||
const draft = this.currentDraft();
|
||||
if (draft) return draft;
|
||||
const flow = this.currentFlow();
|
||||
if (!flow) return undefined;
|
||||
return { name: flow.name, description: flow.description, flow: flow.data };
|
||||
}
|
||||
|
||||
private applyFlowActionResult(result: AssistantFlowActionResult) {
|
||||
const message: AssistantChatMessage = {
|
||||
id: crypto.randomUUID(),
|
||||
role: 'assistant',
|
||||
content: result.message,
|
||||
warnings: result.warnings,
|
||||
validationErrors: result.validationErrors
|
||||
};
|
||||
this.localMessages.update((messages) => [...messages, message]);
|
||||
|
||||
if (!result.flow) return;
|
||||
const session = this.sessionState();
|
||||
if (session) {
|
||||
this.sessionState.set({
|
||||
...session,
|
||||
currentFlow: result.flow,
|
||||
currentDraftFlow: result.flow,
|
||||
lastValidationErrors: result.validationErrors
|
||||
});
|
||||
}
|
||||
this.syncDraftToEditor(result.flow);
|
||||
}
|
||||
|
||||
private backendErrorMessage(error: unknown): string {
|
||||
const value = error as { error?: unknown; message?: unknown };
|
||||
const payload = value?.error;
|
||||
if (typeof payload === 'string' && payload.trim()) return payload;
|
||||
if (payload && typeof payload === 'object') {
|
||||
const body = payload as Record<string, unknown>;
|
||||
for (const key of ['message', 'error', 'detail', 'title']) {
|
||||
if (typeof body[key] === 'string' && body[key].trim()) return body[key] as string;
|
||||
}
|
||||
}
|
||||
if (typeof value?.message === 'string' && value.message.trim()) return value.message;
|
||||
return FlowAssistant.STANDARD_ASSISTANT_ERROR;
|
||||
}
|
||||
|
||||
private async openSession(
|
||||
promptToSend?: string,
|
||||
flowKey = this.activeFlowKey ?? this.resolveFlowKey(this.currentFlow()?.id ?? null)
|
||||
) {
|
||||
this.stopPolling();
|
||||
this.currentCall.set(null);
|
||||
this.localMessages.set([]);
|
||||
this.sessionLoading.set(true);
|
||||
|
||||
this.assistant.createSession({ model }).pipe(
|
||||
this.assistant.createSession(this.sessionRequest()).pipe(
|
||||
take(1),
|
||||
finalize(() => this.sessionLoading.set(false))
|
||||
).subscribe({
|
||||
next: (session) => {
|
||||
this.applySessionState(session);
|
||||
this.persistSnapshot(flowKey);
|
||||
if (promptToSend) this.runFlowAction(promptToSend);
|
||||
},
|
||||
error: (err) => {
|
||||
console.error('Assistant session creation failed', err);
|
||||
this.pushLocalAssistantMessage('Unable to create an assistant session.');
|
||||
const message = this.backendErrorMessage(err);
|
||||
this.assistantErrorMessage.set(message);
|
||||
this.pushLocalAssistantMessage(message);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private startPolling(callId: string, sessionId: string) {
|
||||
this.stopPolling();
|
||||
this.pollSubscription = interval(500).pipe(
|
||||
switchMap(() => this.assistant.getCall(callId))
|
||||
).subscribe({
|
||||
next: (callState) => {
|
||||
this.currentCall.set(callState);
|
||||
this.persistSnapshot();
|
||||
if (this.isTerminalCall(callState)) {
|
||||
this.stopPolling();
|
||||
if (callState.status === 'COMPLETED' && callState.flowResult?.flow) {
|
||||
this.syncDraftToEditor(this.flowResultToDraft(callState.flowResult));
|
||||
}
|
||||
if (callState.status === 'CANCELLED') {
|
||||
this.createPromptSubmitted.set(false);
|
||||
}
|
||||
void this.reloadSession(sessionId, callState.status === 'FAILED');
|
||||
}
|
||||
},
|
||||
error: (err) => {
|
||||
console.error('Assistant call polling failed', err);
|
||||
this.stopPolling();
|
||||
this.handleAssistantErrorWithRetry(this.lastSubmittedPrompt());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private async reloadSession(sessionId: string, hasFailedCall = false) {
|
||||
this.assistant.getSession(sessionId).pipe(
|
||||
take(1)
|
||||
).subscribe({
|
||||
next: (session) => {
|
||||
this.applySessionState(session);
|
||||
this.currentCall.set(null);
|
||||
this.persistSnapshot();
|
||||
if (hasFailedCall) {
|
||||
this.handleAssistantErrorWithRetry(this.lastSubmittedPrompt());
|
||||
}
|
||||
},
|
||||
error: (err) => {
|
||||
console.error('Assistant session refresh failed', err);
|
||||
this.currentCall.set(null);
|
||||
this.handleAssistantErrorWithRetry(this.lastSubmittedPrompt());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private handleAssistantErrorWithRetry(promptForRetry: string) {
|
||||
private handleAssistantErrorWithRetry(
|
||||
promptForRetry: string,
|
||||
message = FlowAssistant.STANDARD_ASSISTANT_ERROR
|
||||
) {
|
||||
const normalizedPrompt = String(promptForRetry ?? '').trim();
|
||||
this.createPromptSubmitted.set(false);
|
||||
this.assistantErrorMessage.set(FlowAssistant.STANDARD_ASSISTANT_ERROR);
|
||||
this.assistantErrorMessage.set(message);
|
||||
this.lastFailedPrompt.set(normalizedPrompt || null);
|
||||
this.pushLocalAssistantMessage(FlowAssistant.STANDARD_ASSISTANT_ERROR);
|
||||
this.pushLocalAssistantMessage(message);
|
||||
this.persistSnapshot();
|
||||
}
|
||||
|
||||
|
|
@ -539,15 +624,6 @@ export class FlowAssistant implements OnInit, OnDestroy {
|
|||
};
|
||||
}
|
||||
|
||||
private flowResultToDraft(result: AssistantFlowResult): AssistantDraftPayload {
|
||||
const currentFlow = this.currentFlow();
|
||||
return {
|
||||
name: result.name ?? currentFlow?.name ?? 'Assistant Draft',
|
||||
description: result.description ?? currentFlow?.description,
|
||||
flow: result.flow
|
||||
};
|
||||
}
|
||||
|
||||
private pushLocalAssistantMessage(content: string) {
|
||||
const normalizedContent = this.normalizeAssistantMessageContent(content);
|
||||
const filtered = this.localMessages().filter((message) => message.role !== 'assistant');
|
||||
|
|
@ -606,8 +682,8 @@ export class FlowAssistant implements OnInit, OnDestroy {
|
|||
id: 'assistant-system-welcome',
|
||||
role: 'system',
|
||||
content: this.canOfferCreate()
|
||||
? (this.isCreateModal() ? 'Describe the workflow you want to create.' : 'Select a model, then ask me to create a new workflow.')
|
||||
: 'Select a model, then ask me to refine, fix, or explain the current workflow.'
|
||||
? (this.isCreateModal() ? 'Describe the workflow you want to create.' : 'Use the default configuration or choose a provider and model, then ask me to create a new workflow.')
|
||||
: 'Use the default configuration or choose a provider and model, then ask me to refine, fix, or explain the current workflow.'
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -625,12 +701,12 @@ export class FlowAssistant implements OnInit, OnDestroy {
|
|||
this.assistantErrorMessage.set(null);
|
||||
this.lastFailedPrompt.set(null);
|
||||
this.lastSubmittedPrompt.set('');
|
||||
const model = this.selectedModel();
|
||||
if (!model) {
|
||||
this.sessionLoading.set(false);
|
||||
return;
|
||||
}
|
||||
void this.openSession(model, flowKey);
|
||||
this.useDefaultConfiguration.set(true);
|
||||
this.selectedProvider.set('');
|
||||
this.selectedModel.set('');
|
||||
this.phaseModels.set(undefined);
|
||||
this.advancedModelsOpen.set(false);
|
||||
this.sessionLoading.set(false);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -642,64 +718,29 @@ export class FlowAssistant implements OnInit, OnDestroy {
|
|||
this.assistantErrorMessage.set(snapshot.assistantErrorMessage);
|
||||
this.lastFailedPrompt.set(snapshot.lastFailedPrompt);
|
||||
this.lastSubmittedPrompt.set(snapshot.lastSubmittedPrompt);
|
||||
this.createPromptSubmitted.set(this.isCreateModal() && this.isActiveCall(snapshot.currentCall));
|
||||
if (snapshot.selectedModel) {
|
||||
this.selectedModel.set(snapshot.selectedModel);
|
||||
}
|
||||
this.createPromptSubmitted.set(false);
|
||||
this.useDefaultConfiguration.set(snapshot.useDefaultConfiguration);
|
||||
this.selectedProvider.set(snapshot.selectedProvider);
|
||||
this.selectedModel.set(snapshot.selectedModel);
|
||||
this.phaseModels.set(snapshot.phaseModels);
|
||||
this.advancedModelsOpen.set(snapshot.advancedModelsOpen);
|
||||
if (snapshot.sessionState) {
|
||||
this.sessionState.set(snapshot.sessionState);
|
||||
} else {
|
||||
this.sessionState.set(null);
|
||||
}
|
||||
|
||||
if (!snapshot.sessionId) {
|
||||
const model = this.selectedModel();
|
||||
if (!model) {
|
||||
this.sessionLoading.set(false);
|
||||
return;
|
||||
}
|
||||
void this.openSession(model, flowKey);
|
||||
return;
|
||||
this.sessionLoading.set(false);
|
||||
if (!this.useDefaultConfiguration() && this.selectedProvider()) {
|
||||
void this.loadProviders();
|
||||
void this.loadModels(this.selectedProvider());
|
||||
}
|
||||
|
||||
if (this.isActiveCall(snapshot.currentCall)) {
|
||||
this.sessionLoading.set(false);
|
||||
this.startPolling(snapshot.currentCall.id, snapshot.currentCall.sessionId || snapshot.sessionId);
|
||||
return;
|
||||
}
|
||||
|
||||
this.sessionLoading.set(true);
|
||||
this.assistant.getSession(snapshot.sessionId).pipe(
|
||||
take(1),
|
||||
finalize(() => this.sessionLoading.set(false))
|
||||
).subscribe({
|
||||
next: (session) => {
|
||||
this.applySessionState(session, {
|
||||
clearLocalMessages: false,
|
||||
syncDraftToEditor: true
|
||||
});
|
||||
},
|
||||
error: (err) => {
|
||||
console.error('Assistant session restore failed', err);
|
||||
const model = this.selectedModel();
|
||||
if (!model) return;
|
||||
void this.openSession(model, flowKey);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private resolveFlowKey(flowId: string | null | undefined): string {
|
||||
return this.sessionStore.flowKey(flowId);
|
||||
}
|
||||
|
||||
private isActiveCall(call: AssistantCallState | null): call is AssistantCallState {
|
||||
return call?.status === 'QUEUED' || call?.status === 'RUNNING';
|
||||
}
|
||||
|
||||
private isTerminalCall(call: AssistantCallState): boolean {
|
||||
return call.status === 'COMPLETED' || call.status === 'FAILED' || call.status === 'CANCELLED';
|
||||
}
|
||||
|
||||
private clearSnapshot(flowKey: string) {
|
||||
if (flowKey === this.activeFlowKey) {
|
||||
this.skipDestroySnapshot = true;
|
||||
|
|
@ -711,6 +752,10 @@ export class FlowAssistant implements OnInit, OnDestroy {
|
|||
this.sessionStore.setSnapshot(flowKey, {
|
||||
sessionId: this.sessionState()?.id ?? null,
|
||||
selectedModel: this.selectedModel(),
|
||||
useDefaultConfiguration: this.useDefaultConfiguration(),
|
||||
selectedProvider: this.selectedProvider(),
|
||||
phaseModels: this.phaseModels(),
|
||||
advancedModelsOpen: this.advancedModelsOpen(),
|
||||
prompt: this.prompt(),
|
||||
modelPickerOpen: this.modelPickerOpen(),
|
||||
quickPromptsOpen: this.quickPromptsOpen(),
|
||||
|
|
@ -771,20 +816,6 @@ export class FlowAssistant implements OnInit, OnDestroy {
|
|||
}
|
||||
}
|
||||
|
||||
private resolveAssistantModelsUrl(url: string): string {
|
||||
if (!url) return url;
|
||||
if (/^https?:\/\//i.test(url)) return url;
|
||||
|
||||
const apiBase = environment.apiUrl;
|
||||
if (/^https?:\/\//i.test(apiBase)) {
|
||||
return new URL(url, `${apiBase.replace(/\/+$/, '')}/`).toString();
|
||||
}
|
||||
|
||||
const origin = typeof window !== 'undefined' ? window.location.origin : '';
|
||||
const normalizedBase = apiBase.startsWith('/') ? apiBase : `/${apiBase}`;
|
||||
return new URL(url, `${origin}${normalizedBase.replace(/\/+$/, '')}/`).toString();
|
||||
}
|
||||
|
||||
private hasMeaningfulFlow(flow: Flow | null): boolean {
|
||||
if (!flow) return false;
|
||||
const data = flow.data;
|
||||
|
|
|
|||
|
|
@ -1,9 +1,13 @@
|
|||
import { Injectable } from '@angular/core';
|
||||
import { AssistantCallState, AssistantChatMessage, AssistantSessionState } from '@models/assistant';
|
||||
import { AssistantCallState, AssistantChatMessage, AssistantLlmSelection, AssistantSessionState } from '@models/assistant';
|
||||
|
||||
export type AssistantSessionSnapshot = {
|
||||
sessionId: string | null;
|
||||
selectedModel: string;
|
||||
useDefaultConfiguration: boolean;
|
||||
selectedProvider: string;
|
||||
phaseModels: AssistantLlmSelection['phaseModels'];
|
||||
advancedModelsOpen: boolean;
|
||||
prompt: string;
|
||||
modelPickerOpen: boolean;
|
||||
quickPromptsOpen: boolean;
|
||||
|
|
@ -85,6 +89,10 @@ export class AssistantSessionStore {
|
|||
return {
|
||||
sessionId: typeof snapshot['sessionId'] === 'string' ? snapshot['sessionId'] : null,
|
||||
selectedModel: typeof snapshot['selectedModel'] === 'string' ? snapshot['selectedModel'] : '',
|
||||
useDefaultConfiguration: snapshot['useDefaultConfiguration'] !== false,
|
||||
selectedProvider: typeof snapshot['selectedProvider'] === 'string' ? snapshot['selectedProvider'] : '',
|
||||
phaseModels: this.normalizePhaseModels(snapshot['phaseModels']),
|
||||
advancedModelsOpen: snapshot['advancedModelsOpen'] === true,
|
||||
prompt: typeof snapshot['prompt'] === 'string' ? snapshot['prompt'] : '',
|
||||
modelPickerOpen: snapshot['modelPickerOpen'] === true,
|
||||
quickPromptsOpen: snapshot['quickPromptsOpen'] !== false,
|
||||
|
|
@ -109,6 +117,17 @@ export class AssistantSessionStore {
|
|||
};
|
||||
}
|
||||
|
||||
private normalizePhaseModels(raw: unknown): AssistantLlmSelection['phaseModels'] {
|
||||
if (!raw || typeof raw !== 'object') return undefined;
|
||||
const value = raw as Record<string, unknown>;
|
||||
const phaseModels = {
|
||||
planningModel: typeof value['planningModel'] === 'string' ? value['planningModel'] : undefined,
|
||||
jsonModel: typeof value['jsonModel'] === 'string' ? value['jsonModel'] : undefined,
|
||||
repairModel: typeof value['repairModel'] === 'string' ? value['repairModel'] : undefined
|
||||
};
|
||||
return Object.values(phaseModels).some(Boolean) ? phaseModels : undefined;
|
||||
}
|
||||
|
||||
private normalizeSessionState(rawSession: unknown): AssistantSessionState | null {
|
||||
if (!rawSession || typeof rawSession !== 'object') return null;
|
||||
const session = rawSession as AssistantSessionState;
|
||||
|
|
|
|||
Loading…
Reference in New Issue