adk-typescript
Version:
TypeScript port of Google's Agent Development Kit (ADK)
190 lines (189 loc) • 7.42 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.GeminiLlmConnection = void 0;
const BaseLlmConnection_1 = require("./BaseLlmConnection");
const LlmResponse_1 = require("./LlmResponse");
/**
* The Gemini model connection.
*/
class GeminiLlmConnection extends BaseLlmConnection_1.BaseLlmConnection {
/**
* Constructor for GeminiLlmConnection
* @param geminiSession The Gemini async session
*/
constructor(geminiSession) {
super();
this.geminiSession = geminiSession;
}
/**
* Sends the conversation history to the gemini model.
*
* You call this method right after setting up the model connection.
* The model will respond if the last content is from user, otherwise it will
* wait for new user input before responding.
*
* @param history The conversation history to send to the model.
*/
async sendHistory(history) {
// Filter out any content without text parts (e.g., ignore audio during agent transfer)
const contents = history.filter(content => content.parts &&
content.parts.length > 0 &&
content.parts.some(part => part.text));
if (contents.length > 0) {
await this.geminiSession.send({
turns: contents,
turn_complete: contents[contents.length - 1].role === 'user'
});
}
else {
console.info('No content is sent');
}
}
/**
* Sends a user content to the gemini model.
*
* The model will respond immediately upon receiving the content.
* If you send function responses, all parts in the content should be function
* responses.
*
* @param content The content to send to the model.
*/
async sendContent(content) {
if (!content.parts || content.parts.length === 0) {
throw new Error('Content must have parts');
}
if (content.parts[0].functionResponse) {
// All parts have to be function responses
const functionResponses = content.parts
.map(part => part.functionResponse)
.filter(fr => fr !== undefined);
console.debug('Sending LLM function response:', functionResponses);
await this.geminiSession.send({
function_responses: functionResponses
});
}
else {
await this.geminiSession.send({
turns: [content],
turn_complete: true
});
}
}
/**
* Sends a chunk of audio or a frame of video to the model in realtime.
*
* The model may not respond immediately upon receiving the blob. It will do
* voice activity detection and decide when to respond.
*
* @param blob The blob to send to the model.
*/
async sendRealtime(blob) {
console.debug('Sending LLM Blob:', blob);
await this.geminiSession.send(blob);
}
/**
* Builds a full text response.
*
* The text is not partial and the returned LlmResponse is not be
* partial.
*
* @param text The text to be included in the response.
* @returns An LlmResponse containing the full text.
*/
buildFullTextResponse(text) {
const response = new LlmResponse_1.LlmResponse();
response.content = {
role: 'model',
parts: [{ text }]
};
return response;
}
/**
* Receives the model response using the llm server connection.
*
* @returns An async generator yielding LlmResponse objects.
*/
async *receive() {
let text = '';
for await (const message of this.geminiSession.receive()) {
console.debug('Got LLM Live message:', message);
if (message.server_content) {
const serverContent = message.server_content;
const content = serverContent.model_turn;
if (content && content.parts && content.parts.length > 0) {
const response = new LlmResponse_1.LlmResponse();
response.content = content;
response.interrupted = serverContent.interrupted;
if (content.parts[0].text) {
text += content.parts[0].text;
response.partial = true;
}
// Don't yield the merged text event when receiving audio data
else if (text && !content.parts[0].inlineData) {
yield this.buildFullTextResponse(text);
text = '';
}
yield response;
}
// Handle transcription
if (serverContent.output_transcription &&
serverContent.output_transcription.text) {
// Transcription is always considered as partial event
// We rely on other control signals to determine when to yield the
// full text response (turn_complete, interrupted, or tool_call)
text += serverContent.output_transcription.text;
const response = new LlmResponse_1.LlmResponse();
response.content = {
role: 'model',
parts: [{ text: serverContent.output_transcription.text }]
};
response.partial = true;
yield response;
}
// Handle turn complete
if (serverContent.turn_complete) {
if (text) {
yield this.buildFullTextResponse(text);
text = '';
}
const response = new LlmResponse_1.LlmResponse();
response.turnComplete = true;
response.interrupted = serverContent.interrupted;
yield response;
break;
}
// In case of empty content or parts, we still surface it
// If it's an interrupted message, we merge the previous partial text
if (serverContent.interrupted && text) {
yield this.buildFullTextResponse(text);
text = '';
}
// Yield interrupted status
const interruptedResponse = new LlmResponse_1.LlmResponse();
interruptedResponse.interrupted = serverContent.interrupted;
yield interruptedResponse;
}
// Handle tool call
if (message.tool_call) {
if (text) {
yield this.buildFullTextResponse(text);
text = '';
}
const parts = message.tool_call.function_calls.map(functionCall => ({ functionCall }));
const response = new LlmResponse_1.LlmResponse();
response.content = {
role: 'model',
parts
};
yield response;
}
}
}
/**
* Closes the llm server connection.
*/
async close() {
await this.geminiSession.close();
}
}
exports.GeminiLlmConnection = GeminiLlmConnection;