adk-typescript
Version:
TypeScript port of Google's Agent Development Kit (ADK)
432 lines (431 loc) • 15.5 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.requestProcessor = void 0;
exports.getContents = getContents;
const LlmAgent_1 = require("../../agents/LlmAgent");
const Event_1 = require("../../events/Event");
// Constants
const REQUEST_EUC_FUNCTION_CALL_NAME = 'adk_request_credential';
/**
* Safely gets function responses from an event
*
* @param event The event to get function responses from
* @returns An array of function responses
*/
function safeGetFunctionResponses(event) {
if (typeof event.getFunctionResponses === 'function') {
return event.getFunctionResponses();
}
// Fallback implementation if method doesn't exist
if (event.content && event.content.parts) {
const responses = [];
for (const part of event.content.parts) {
if (part.functionResponse) {
responses.push(part.functionResponse);
}
}
return responses;
}
return [];
}
/**
* Safely gets function calls from an event
*
* @param event The event to get function calls from
* @returns An array of function calls
*/
function safeGetFunctionCalls(event) {
if (typeof event.getFunctionCalls === 'function') {
return event.getFunctionCalls();
}
// Fallback implementation if method doesn't exist
if (event.content && event.content.parts) {
const calls = [];
for (const part of event.content.parts) {
if (part.functionCall) {
calls.push(part.functionCall);
}
}
return calls;
}
return [];
}
/**
* Removes client function call IDs from all function calls in content
*
* @param content The content containing function calls to clean
*/
function removeClientFunctionCallId(content) {
if (!content || !content.parts) {
return;
}
for (const part of content.parts) {
if (part.functionCall) {
// Remove the id property from the function call
delete part.functionCall.id;
}
}
}
/**
* Builds the contents for the LLM request.
*/
class ContentLlmRequestProcessor {
/**
* Runs the processor asynchronously.
*
* @param invocationContext The invocation context
* @param llmRequest The LLM request to process
* @returns An async generator yielding events
*/
async *runAsync(invocationContext, llmRequest) {
const agent = invocationContext.agent;
if (!(agent instanceof LlmAgent_1.LlmAgent)) {
return;
}
if (agent.includeContents !== 'none') {
llmRequest.contents = getContents(invocationContext.branch, invocationContext.session.events, agent.name);
}
// Maintain async generator contract without using constant condition
return;
// The following is unreachable but satisfies TypeScript's return type
yield {};
}
}
/**
* The main content request processor instance.
*/
exports.requestProcessor = new ContentLlmRequestProcessor();
/**
* Rearranges the async function_response events in the history.
*
* @param events List of events to rearrange
* @returns Rearranged list of events
*/
function rearrangeEventsForAsyncFunctionResponsesInHistory(events) {
const functionCallIdToResponseEventsIndex = new Map();
for (let i = 0; i < events.length; i++) {
const functionResponses = safeGetFunctionResponses(events[i]);
if (functionResponses.length > 0) {
for (const functionResponse of functionResponses) {
if (functionResponse.id) {
functionCallIdToResponseEventsIndex.set(functionResponse.id, i);
}
}
}
}
const resultEvents = [];
for (const event of events) {
if (safeGetFunctionResponses(event).length > 0) {
// function_response should be handled together with function_call below.
continue;
}
else if (safeGetFunctionCalls(event).length > 0) {
const functionResponseEventsIndices = new Set();
for (const functionCall of safeGetFunctionCalls(event)) {
const functionCallId = functionCall.id;
if (functionCallId && functionCallIdToResponseEventsIndex.has(functionCallId)) {
functionResponseEventsIndices.add(functionCallIdToResponseEventsIndex.get(functionCallId));
}
}
resultEvents.push(event);
if (functionResponseEventsIndices.size === 0) {
continue;
}
if (functionResponseEventsIndices.size === 1) {
const index = Array.from(functionResponseEventsIndices)[0];
resultEvents.push(events[index]);
}
else {
// Merge all async function_response as one response event
resultEvents.push(mergeFunctionResponseEvents(Array.from(functionResponseEventsIndices)
.sort()
.map(i => events[i])));
}
}
else {
resultEvents.push(event);
}
}
return resultEvents;
}
/**
* Rearranges the events for the latest function_response.
*
* If the latest function_response is for an async function_call, all events
* between the initial function_call and the latest function_response will be
* removed.
*
* @param events List of events to rearrange
* @returns Rearranged list of events
*/
function rearrangeEventsForLatestFunctionResponse(events) {
if (!events || events.length === 0) {
return events;
}
const functionResponses = safeGetFunctionResponses(events[events.length - 1]);
if (functionResponses.length === 0) {
// No need to process, since the latest event is not function_response.
return events;
}
const functionResponsesIds = new Set();
for (const functionResponse of functionResponses) {
if (functionResponse.id) {
functionResponsesIds.add(functionResponse.id);
}
}
if (events.length >= 2) {
const functionCalls = safeGetFunctionCalls(events[events.length - 2]);
if (functionCalls.length > 0) {
for (const functionCall of functionCalls) {
// The latest function_response is already matched
if (functionCall.id && functionResponsesIds.has(functionCall.id)) {
return events;
}
}
}
}
let functionCallEventIdx = -1;
// look for corresponding function call event reversely
for (let idx = events.length - 2; idx >= 0; idx--) {
const event = events[idx];
const functionCalls = safeGetFunctionCalls(event);
if (functionCalls.length > 0) {
for (const functionCall of functionCalls) {
if (functionCall.id && functionResponsesIds.has(functionCall.id)) {
functionCallEventIdx = idx;
break;
}
}
if (functionCallEventIdx !== -1) {
// in case the last response event only have part of the responses
// for the function calls in the function call event
for (const functionCall of functionCalls) {
if (functionCall.id) {
functionResponsesIds.add(functionCall.id);
}
}
break;
}
}
}
if (functionCallEventIdx === -1) {
throw new Error(`No function call event found for function responses ids: ${Array.from(functionResponsesIds).join(', ')}`);
}
// collect all function response between last function response event
// and function call event
const functionResponseEvents = [];
for (let idx = functionCallEventIdx + 1; idx < events.length - 1; idx++) {
const event = events[idx];
const functionResponses = safeGetFunctionResponses(event);
if (functionResponses.length > 0 &&
functionResponses[0].id &&
functionResponsesIds.has(functionResponses[0].id)) {
functionResponseEvents.push(event);
}
}
functionResponseEvents.push(events[events.length - 1]);
const resultEvents = events.slice(0, functionCallEventIdx + 1);
resultEvents.push(mergeFunctionResponseEvents(functionResponseEvents));
return resultEvents;
}
/**
* Merges multiple function response events into a single event.
*
* @param events List of function response events to merge
* @returns A merged event containing all function responses
*/
function mergeFunctionResponseEvents(events) {
if (!events || events.length === 0) {
throw new Error('No events to merge');
}
if (events.length === 1) {
return events[0];
}
// Create a new merged event based on the first event
const mergedEvent = new Event_1.Event({
id: events[0].id,
timestamp: events[0].timestamp,
author: events[0].author,
invocationId: events[0].invocationId,
branch: events[0].branch,
content: {
role: events[0].content?.role || 'function',
parts: []
}
});
// Collect all function responses from all events
for (const event of events) {
const functionResponses = safeGetFunctionResponses(event);
if (functionResponses.length > 0) {
for (const functionResponse of functionResponses) {
if (mergedEvent.content) {
mergedEvent.content.parts.push({
functionResponse: functionResponse
});
}
}
}
}
return mergedEvent;
}
/**
* Gets the contents for the LLM request.
*
* @param currentBranch The current branch of the agent
* @param events List of events
* @param agentName The name of the agent
* @returns List of contents
*/
function getContents(currentBranch, events, agentName = '') {
let filteredEvents = [];
// Parse the events, leaving the contents and the function calls and
// responses from the current agent.
for (const event of events) {
if (!event.content || !event.content.role || !event.content.parts || event.content.parts[0].text == '') {
// Skip events without content, or generated neither by user nor by model.
// E.g. events purely for mutating session states.
continue;
}
if (!isEventBelongsToBranch(currentBranch, event)) {
// Skip events not belong to current branch.
continue;
}
if (isAuthEvent(event)) {
// skip auth event
continue;
}
filteredEvents.push(isOtherAgentReply(agentName, event)
? convertForeignEvent(event)
: event);
}
filteredEvents = rearrangeEventsForLatestFunctionResponse(filteredEvents);
filteredEvents = rearrangeEventsForAsyncFunctionResponsesInHistory(filteredEvents);
const contents = [];
for (const event of filteredEvents) {
if (event.content) {
// Clone the content to avoid modifying the original event
const content = JSON.parse(JSON.stringify(event.content));
// Ensure there are valid parts and filter out empty ones
if (content.parts) {
// Filter out empty parts or parts without required fields
content.parts = content.parts.filter(part => {
// Keep parts with valid text
if (part.text !== undefined && part.text !== null) {
return true;
}
// Keep parts with valid function calls
if (part.functionCall && part.functionCall.name) {
return true;
}
// Keep parts with valid function responses
if (part.functionResponse && part.functionResponse.name) {
return true;
}
// If we reached here, this part doesn't have valid required fields
return false;
});
// Only add content if it has at least one valid part
if (content.parts.length > 0) {
removeClientFunctionCallId(content);
contents.push(content);
}
}
}
}
return contents;
}
/**
* Whether the event is a reply from another agent.
*
* @param currentAgentName The name of the current agent
* @param event The event to check
* @returns True if the event is a reply from another agent
*/
function isOtherAgentReply(currentAgentName, event) {
return Boolean(currentAgentName &&
event.author !== currentAgentName &&
event.author !== 'user');
}
/**
* Converts an event authored by another agent as a user-content event.
*
* This is to provide another agent's output as context to the current agent, so
* that current agent can continue to respond, such as summarizing previous
* agent's reply, etc.
*
* @param event The event to convert
* @returns The converted event
*/
function convertForeignEvent(event) {
if (!event.content || !event.content.parts) {
return event;
}
const content = {
role: 'user',
parts: [{ text: 'For context:' }]
};
for (const part of event.content.parts) {
if (part.text) {
content.parts.push({
text: `[${event.author}] said: ${part.text}`
});
}
else if (part.functionCall) {
content.parts.push({
text: `[${event.author}] called tool \`${part.functionCall.name}\` with parameters: ${JSON.stringify(part.functionCall.args)}`
});
}
else if (part.functionResponse) {
content.parts.push({
text: `[${event.author}] \`${part.functionResponse.name}\` tool returned result: ${JSON.stringify(part.functionResponse.response)}`
});
}
else {
// Fallback to the original part for non-text and non-functionCall parts.
content.parts.push(part);
}
}
return new Event_1.Event({
timestamp: event.timestamp,
author: 'user',
content: content,
branch: event.branch,
});
}
/**
* Checks if an event belongs to a branch.
*
* Event belongs to a branch when event.branch is prefix of the invocation branch.
*
* @param invocationBranch The invocation branch
* @param event The event to check
* @returns True if the event belongs to the branch
*/
function isEventBelongsToBranch(invocationBranch, event) {
if (!invocationBranch || !event.branch) {
return true;
}
return invocationBranch.startsWith(event.branch);
}
/**
* Checks if an event is an auth event.
*
* @param event The event to check
* @returns True if the event is an auth event
*/
function isAuthEvent(event) {
if (!event.content?.parts) {
return false;
}
for (const part of event.content.parts) {
if (part.functionCall &&
part.functionCall.name === REQUEST_EUC_FUNCTION_CALL_NAME) {
return true;
}
if (part.functionResponse &&
part.functionResponse.name === REQUEST_EUC_FUNCTION_CALL_NAME) {
return true;
}
}
return false;
}