MCP Terminal Server
by dillip285
/**
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { StreamingCallback, z } from '@genkit-ai/core';
import { Channel } from '@genkit-ai/core/async';
import {
ATTR_PREFIX,
SPAN_TYPE_ATTR,
runInNewSpan,
} from '@genkit-ai/core/tracing';
import {
GenerateOptions,
GenerateResponse,
GenerateResponseChunk,
GenerateStreamOptions,
GenerateStreamResponse,
GenerationCommonConfigSchema,
MessageData,
Part,
generate,
} from './index.js';
import {
BaseGenerateOptions,
Session,
SessionStore,
runWithSession,
} from './session.js';
export const MAIN_THREAD = 'main';
export const SESSION_ID_ATTR = `${ATTR_PREFIX}:sessionId`;
export const THREAD_NAME_ATTR = `${ATTR_PREFIX}:threadName`;
export type ChatGenerateOptions<
O extends z.ZodTypeAny = z.ZodTypeAny,
CustomOptions extends z.ZodTypeAny = z.ZodTypeAny,
> = GenerateOptions<O, CustomOptions>;
export interface PromptRenderOptions<I> {
input?: I;
}
export type ChatOptions<I = undefined, S = any> = (
| PromptRenderOptions<I>
| BaseGenerateOptions
) & {
store?: SessionStore<S>;
sessionId?: string;
};
/**
* Chat encapsulates a statful execution environment for chat.
* Chat session executed within a session in this environment will have acesss to
* session convesation history.
*
* ```ts
* const ai = genkit({...});
* const chat = ai.chat(); // create a Chat
* let response = await chat.send('hi, my name is Genkit');
* response = await chat.send('what is my name?'); // chat history aware conversation
* ```
*/
export class Chat {
private requestBase?: Promise<BaseGenerateOptions>;
readonly sessionId: string;
private _messages?: MessageData[];
private threadName: string;
constructor(
readonly session: Session,
requestBase: Promise<BaseGenerateOptions>,
options: {
id: string;
thread: string;
messages?: MessageData[];
}
) {
this.sessionId = options.id;
this.threadName = options.thread;
this.requestBase = requestBase?.then((rb) => {
const requestBase = { ...rb };
// this is handling dotprompt render case
if (requestBase && requestBase['prompt']) {
const basePrompt = requestBase['prompt'] as string | Part | Part[];
let promptMessage: MessageData;
if (typeof basePrompt === 'string') {
promptMessage = {
role: 'user',
content: [{ text: basePrompt }],
};
} else if (Array.isArray(basePrompt)) {
promptMessage = {
role: 'user',
content: basePrompt,
};
} else {
promptMessage = {
role: 'user',
content: [basePrompt],
};
}
requestBase.messages = [...(requestBase.messages ?? []), promptMessage];
}
if (hasPreamble(requestBase.messages)) {
requestBase.messages = [
// if request base contains a preamble, always put it first
...(getPreamble(requestBase.messages) ?? []),
// strip out the preamble from history
...(stripPreamble(options.messages) ?? []),
// add whatever non-preamble remains from request
...(stripPreamble(requestBase.messages) ?? []),
];
} else {
requestBase.messages = [
...(options.messages ?? []),
...(requestBase.messages ?? []),
];
}
this._messages = requestBase.messages;
return requestBase;
});
this._messages = options.messages;
}
async send<
O extends z.ZodTypeAny = z.ZodTypeAny,
CustomOptions extends z.ZodTypeAny = typeof GenerationCommonConfigSchema,
>(
options: string | Part[] | ChatGenerateOptions<O, CustomOptions>
): Promise<GenerateResponse<z.infer<O>>> {
return runWithSession(this.session.registry, this.session, () =>
runInNewSpan(
this.session.registry,
{
metadata: {
name: 'send',
},
labels: {
[SPAN_TYPE_ATTR]: 'helper',
[SESSION_ID_ATTR]: this.session.id,
[THREAD_NAME_ATTR]: this.threadName,
},
},
async (metadata) => {
let resolvedOptions = resolveSendOptions(options);
let streamingCallback:
| StreamingCallback<GenerateResponseChunk>
| undefined = undefined;
if (resolvedOptions.onChunk || resolvedOptions.streamingCallback) {
streamingCallback =
resolvedOptions.onChunk ?? resolvedOptions.streamingCallback;
}
let request: GenerateOptions = {
...(await this.requestBase),
messages: this.messages,
...resolvedOptions,
};
metadata.input = resolvedOptions;
let response = await generate(this.session.registry, {
...request,
onChunk: streamingCallback,
});
this.requestBase = Promise.resolve({
...(await this.requestBase),
// these things may get changed by tools calling within generate.
tools: response?.request?.tools?.map((td) => td.name),
toolChoice: response?.request?.toolChoice,
config: response?.request?.config,
});
await this.updateMessages(response.messages);
metadata.output = JSON.stringify(response);
return response;
}
)
);
}
sendStream<
O extends z.ZodTypeAny = z.ZodTypeAny,
CustomOptions extends z.ZodTypeAny = typeof GenerationCommonConfigSchema,
>(
options: string | Part[] | GenerateStreamOptions<O, CustomOptions>
): GenerateStreamResponse<z.infer<O>> {
let channel = new Channel<GenerateResponseChunk>();
let resolvedOptions = resolveSendOptions(options);
const sent = this.send({
...resolvedOptions,
onChunk: (chunk) => channel.send(chunk),
});
sent.then(
() => channel.close(),
(err) => channel.error(err)
);
return {
response: sent,
stream: channel,
};
}
get messages(): MessageData[] {
return this._messages ?? [];
}
private async updateMessages(messages: MessageData[]): Promise<void> {
this._messages = messages;
await this.session.updateMessages(this.threadName, messages);
}
}
function hasPreamble(msgs?: MessageData[]) {
return !!msgs?.find((m) => m.metadata?.preamble);
}
function getPreamble(msgs?: MessageData[]) {
return msgs?.filter((m) => m.metadata?.preamble);
}
function stripPreamble(msgs?: MessageData[]) {
return msgs?.filter((m) => !m.metadata?.preamble);
}
function resolveSendOptions<
O extends z.ZodTypeAny,
CustomOptions extends z.ZodTypeAny,
>(
options: string | Part[] | ChatGenerateOptions<O, CustomOptions>
): ChatGenerateOptions<O, CustomOptions> {
let resolvedOptions: ChatGenerateOptions<O, CustomOptions>;
// string
if (typeof options === 'string') {
resolvedOptions = {
prompt: options,
} as ChatGenerateOptions<O, CustomOptions>;
} else if (Array.isArray(options)) {
// Part[]
resolvedOptions = {
prompt: options,
} as ChatGenerateOptions<O, CustomOptions>;
} else {
resolvedOptions = options as ChatGenerateOptions<O, CustomOptions>;
}
return resolvedOptions;
}