diff --git a/packages/ai-chat/src/common/chat-session-naming-service.ts b/packages/ai-chat/src/common/chat-session-naming-service.ts index 35f3e055baa3c..5d1d66c569e17 100644 --- a/packages/ai-chat/src/common/chat-session-naming-service.ts +++ b/packages/ai-chat/src/common/chat-session-naming-service.ts @@ -106,7 +106,7 @@ export class ChatSessionNamingAgent implements Agent { const sessionId = generateUuid(); const requestId = generateUuid(); - const request: UserRequest = { + const request: UserRequest & { agentId: string } = { messages: [{ actor: 'user', text: message, diff --git a/packages/ai-core/src/browser/frontend-language-model-service.ts b/packages/ai-core/src/browser/frontend-language-model-service.ts index 71a2fe85f4de1..1608a43da91ff 100644 --- a/packages/ai-core/src/browser/frontend-language-model-service.ts +++ b/packages/ai-core/src/browser/frontend-language-model-service.ts @@ -54,7 +54,7 @@ export class FrontendLanguageModelServiceImpl extends LanguageModelServiceImpl { } } -export const mergeRequestSettings = (requestSettings: RequestSetting[], modelId: string, providerId: string, agentId: string): RequestSetting => { +export const mergeRequestSettings = (requestSettings: RequestSetting[], modelId: string, providerId: string, agentId?: string): RequestSetting => { const prioritizedSettings = Prioritizeable.prioritizeAllSync(requestSettings, setting => getRequestSettingSpecificity(setting, { modelId, diff --git a/packages/ai-core/src/common/language-model-interaction-model.ts b/packages/ai-core/src/common/language-model-interaction-model.ts new file mode 100644 index 0000000000000..2d552d0164fae --- /dev/null +++ b/packages/ai-core/src/common/language-model-interaction-model.ts @@ -0,0 +1,93 @@ +// ***************************************************************************** +// Copyright (C) 2025 STMicroelectronics and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// http://www.eclipse.org/legal/epl-2.0. +// +// This Source Code may also be made available under the following Secondary +// Licenses when the conditions for such availability set forth in the Eclipse +// Public License v. 2.0 are satisfied: GNU General Public License, version 2 +// with the GNU Classpath Exception which is available at +// https://www.gnu.org/software/classpath/license.html. +// +// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 +// ***************************************************************************** +import { + LanguageModelParsedResponse, + LanguageModelRequest, + LanguageModelStreamResponsePart, + LanguageModelTextResponse +} from './language-model'; + +/** + * A session tracking raw exchanges with language models, organized into exchange units. + */ +export interface LanguageModelSession { + /** + * Identifier of this Language Model Session. Corresponds to Chat session ids + */ + id: string; + /** + * All exchange units part of this session + */ + exchanges: LanguageModelExchange[]; +} + +/** + * An exchange unit representing a logical operation which may involve multiple model requests. + */ +export interface LanguageModelExchange { + /** + * Identifier of the exchange unit. + */ + id: string; + /** + * All requests that constitute this exchange + */ + requests: LanguageModelExchangeRequest[]; + /** + * Arbitrary metadata for the exchange + */ + metadata: { + agent?: string; + [key: string]: unknown; + } +} + +/** + * Alternative to the LanguageModelStreamResponse, suited for inspection + */ +export interface LanguageModelMonitoredStreamResponse { + parts: LanguageModelStreamResponsePart[]; +} + +/** + * Represents a request to a language model within an exchange unit, capturing the request and its response. + */ +export interface LanguageModelExchangeRequest { + /** + * Identifier of the request. Might share the id with the parent exchange if there's only one request. + */ + id: string; + /** + * The actual request sent to the language model + */ + request: LanguageModelRequest; + /** + * Arbitrary metadata for the request. Might contain an agent id and timestamp. + */ + metadata: { + agent?: string; + timestamp?: number; + [key: string]: unknown; + }; + /** + * The identifier of the language model the request was sent to + */ + languageModel: string; + /** + * The recorded response + */ + response: LanguageModelTextResponse | LanguageModelParsedResponse | LanguageModelMonitoredStreamResponse; +} diff --git a/packages/ai-core/src/common/language-model-service.ts b/packages/ai-core/src/common/language-model-service.ts index ff6d744610866..a5baa2b749f2c 100644 --- a/packages/ai-core/src/common/language-model-service.ts +++ b/packages/ai-core/src/common/language-model-service.ts @@ -15,13 +15,29 @@ // ***************************************************************************** import { inject } from '@theia/core/shared/inversify'; -import { LanguageModel, LanguageModelRegistry, LanguageModelResponse, UserRequest } from './language-model'; -import { CommunicationRecordingService } from './communication-recording-service'; +import { isLanguageModelStreamResponse, LanguageModel, LanguageModelRegistry, LanguageModelResponse, LanguageModelStreamResponsePart, UserRequest } from './language-model'; +import { LanguageModelExchangeRequest, LanguageModelSession } from './language-model-interaction-model'; +import { Emitter } from '@theia/core'; + +export interface RequestAddedEvent { + type: 'requestAdded', + id: string; +} +export interface ResponseCompletedEvent { + type: 'responseCompleted', + requestId: string; +} +export type SessionEvent = RequestAddedEvent | ResponseCompletedEvent; export const LanguageModelService = Symbol('LanguageModelService'); export interface LanguageModelService { + onSessionChanged: Emitter['event']; + /** + * Collection of all recorded LanguageModelSessions. + */ + sessions: LanguageModelSession[]; /** - * Submit a language model request in the context of the given `chatRequest`. + * Submit a language model request, it will automatically be recorded within a LanguageModelSession. */ sendRequest( languageModel: LanguageModel, @@ -33,8 +49,10 @@ export class LanguageModelServiceImpl implements LanguageModelService { @inject(LanguageModelRegistry) protected languageModelRegistry: LanguageModelRegistry; - @inject(CommunicationRecordingService) - protected recordingService: CommunicationRecordingService; + sessions: LanguageModelSession[] = []; + + protected sessionChangedEmitter = new Emitter(); + onSessionChanged = this.sessionChangedEmitter.event; async sendRequest( languageModel: LanguageModel, @@ -53,7 +71,84 @@ export class LanguageModelServiceImpl implements LanguageModelService { return true; }); - return languageModel.request(languageModelRequest, languageModelRequest.cancellationToken); + let response = await languageModel.request(languageModelRequest, languageModelRequest.cancellationToken); + let storedResponse: LanguageModelExchangeRequest['response']; + if (isLanguageModelStreamResponse(response)) { + const parts: LanguageModelStreamResponsePart[] = []; + response = { + ...response, + stream: createLoggingAsyncIterable(response.stream, + parts, + () => this.sessionChangedEmitter.fire({ type: 'responseCompleted', requestId: languageModelRequest.subRequestId ?? languageModelRequest.requestId })) + }; + storedResponse = { parts }; + } else { + storedResponse = response; + } + this.storeRequest(languageModel, languageModelRequest, storedResponse); + + return response; } + protected storeRequest(languageModel: LanguageModel, languageModelRequest: UserRequest, response: LanguageModelExchangeRequest['response']): void { + // Find or create the session for this request + let session = this.sessions.find(s => s.id === languageModelRequest.sessionId); + if (!session) { + session = { + id: languageModelRequest.sessionId, + exchanges: [] + }; + this.sessions.push(session); + } + + // Find or create the exchange for this request + let exchange = session.exchanges.find(r => r.id === languageModelRequest.requestId); + if (!exchange) { + exchange = { + id: languageModelRequest.requestId, + requests: [], + metadata: { agent: languageModelRequest.agentId } + }; + session.exchanges.push(exchange); + } + + // Create and add the LanguageModelExchangeRequest to the exchange + const exchangeRequest: LanguageModelExchangeRequest = { + id: languageModelRequest.subRequestId ?? languageModelRequest.requestId, + request: languageModelRequest, + languageModel: languageModel.id, + response: response, + metadata: {} + }; + + exchange.requests.push(exchangeRequest); + + exchangeRequest.metadata.agent = languageModelRequest.agentId; + exchangeRequest.metadata.timestamp = Date.now(); + + this.sessionChangedEmitter.fire({ type: 'requestAdded', id: languageModelRequest.subRequestId ?? languageModelRequest.requestId }); + } + +} + +/** + * Creates an AsyncIterable wrapper that stores each yielded item while preserving the + * original AsyncIterable behavior. + */ +async function* createLoggingAsyncIterable( + stream: AsyncIterable, + parts: LanguageModelStreamResponsePart[], + streamFinished: () => void +): AsyncIterable { + try { + for await (const part of stream) { + parts.push(part); + yield part; + } + } catch (error) { + parts.push({ content: `[NOT FROM LLM] An error occured: ${error.message}` }); + throw error; + } finally { + streamFinished(); + } } diff --git a/packages/ai-core/src/common/language-model.ts b/packages/ai-core/src/common/language-model.ts index f9ebd4f9e8c9d..3686dd994c96b 100644 --- a/packages/ai-core/src/common/language-model.ts +++ b/packages/ai-core/src/common/language-model.ts @@ -159,10 +159,32 @@ export interface ResponseFormatJsonSchema { }; } +/** + * The UserRequest extends the "pure" LanguageModelRequest for cancelling support as well as + * logging metadata. + * The additional metadata might also be used for other use cases, for example to query default + * request settings based on the agent id, merging with the request settings handed over. + */ export interface UserRequest extends LanguageModelRequest { + /** + * Identifier of the Ai/ChatSession + */ sessionId: string; + /** + * Identifier of the semantic request. Corresponds to request id in Chat sessions + */ requestId: string; - agentId: string; + /** + * Id of a sub request in case a semantic request consists of multiple sub requests + */ + subRequestId?: string; + /** + * Optional agent identifier in case the request was sent by an agent + */ + agentId?: string; + /** + * Cancellation support + */ cancellationToken?: CancellationToken; }