diff --git a/ts/packages/agents/calendar/src/calendarActionHandler.ts b/ts/packages/agents/calendar/src/calendarActionHandler.ts index 0bd355d7..e6c6a5e0 100644 --- a/ts/packages/agents/calendar/src/calendarActionHandler.ts +++ b/ts/packages/agents/calendar/src/calendarActionHandler.ts @@ -52,7 +52,7 @@ export class CalendarClientLoginCommandHandler } const handlers: CommandHandlerTable = { - description: "Calendar login commmand", + description: "Calendar login command", defaultSubCommand: "login", commands: { login: new CalendarClientLoginCommandHandler(), diff --git a/ts/packages/agents/test/src/handler.ts b/ts/packages/agents/test/src/handler.ts index 74cd317f..e0930977 100644 --- a/ts/packages/agents/test/src/handler.ts +++ b/ts/packages/agents/test/src/handler.ts @@ -1,13 +1,46 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -import { ActionContext, AppAgent } from "@typeagent/agent-sdk"; +import { + ActionContext, + AppAgent, + ParsedCommandParams, +} from "@typeagent/agent-sdk"; import { AddAction } from "./schema.js"; import { createActionResult } from "@typeagent/agent-sdk/helpers/action"; +import { + CommandHandler, + getCommandInterface, +} from "@typeagent/agent-sdk/helpers/command"; +class RequestCommandHandler implements CommandHandler { + public readonly description = "Request a test"; + public readonly parameters = { + args: { + test: { + description: "Test to request", + implicitQuotes: true, + }, + }, + } as const; + public async run( + context: ActionContext, + params: ParsedCommandParams, + ) { + context.actionIO.setDisplay(params.args.test); + } +} + +const handlers = { + description: "Test App Agent Commands", + commands: { + request: new RequestCommandHandler(), + }, +}; export function instantiate(): AppAgent { return { executeAction, + ...getCommandInterface(handlers), }; } diff --git a/ts/packages/dispatcher/src/command/command.ts b/ts/packages/dispatcher/src/command/command.ts index 878fa7d7..5fcffac5 100644 --- a/ts/packages/dispatcher/src/command/command.ts +++ b/ts/packages/dispatcher/src/command/command.ts @@ -157,8 +157,8 @@ async function parseCommand( ) { let input = originalInput.trim(); if (!input.startsWith("@")) { - // default to dispatcher request - input = `dispatcher request ${input}`; + const requestHandlerAgent = context.session.getConfig().request; + input = `${requestHandlerAgent} request ${input}`; } else { input = input.substring(1); } @@ -281,6 +281,10 @@ export const enum unicodeChar { convert = "🔄", } export function getSettingSummary(context: CommandHandlerContext) { + if (context.session.getConfig().request !== DispatcherName) { + const requestAgentName = context.session.getConfig().request; + return `{{${context.agents.getActionConfig(requestAgentName).emojiChar} ${requestAgentName.toUpperCase()}}}`; + } const prompt: string[] = [unicodeChar.robotFace]; const names = context.agents.getActiveSchemas(); diff --git a/ts/packages/dispatcher/src/handlers/common/commandHandlerContext.ts b/ts/packages/dispatcher/src/handlers/common/commandHandlerContext.ts index 2bf3aa30..17a4cb8b 100644 --- a/ts/packages/dispatcher/src/handlers/common/commandHandlerContext.ts +++ b/ts/packages/dispatcher/src/handlers/common/commandHandlerContext.ts @@ -241,7 +241,7 @@ function getLoggerSink(isDbEnabled: () => boolean, clientIO: ClientIO) { ); } -async function addAppAgentProvidres( +async function addAppAgentProviders( context: CommandHandlerContext, appAgentProviders?: AppAgentProvider[], cacheDirPath?: string, @@ -335,7 +335,7 @@ export async function initializeCommandHandlerContext( // Runtime context commandLock: createLimiter(1), // Make sure we process one command at a time. agentCache: await getAgentCache(session, agents, logger), - lastActionSchemaName: "", + lastActionSchemaName: DispatcherName, translatorCache: new Map(), currentScriptDir: process.cwd(), chatHistory: createChatHistory(), @@ -345,7 +345,7 @@ export async function initializeCommandHandlerContext( batchMode: false, }; - await addAppAgentProvidres( + await addAppAgentProviders( context, options?.appAgentProviders, cacheDirPath, diff --git a/ts/packages/dispatcher/src/handlers/configCommandHandlers.ts b/ts/packages/dispatcher/src/handlers/configCommandHandlers.ts index 1aa2a428..53ce60b0 100644 --- a/ts/packages/dispatcher/src/handlers/configCommandHandlers.ts +++ b/ts/packages/dispatcher/src/handlers/configCommandHandlers.ts @@ -33,6 +33,7 @@ import { } from "@typeagent/agent-sdk/helpers/display"; import { alwaysEnabledAgents } from "../agent/appAgentManager.js"; import { getCacheFactory } from "../internal.js"; +import { resolveCommand } from "../command/command.js"; const enum AgentToggle { Schema, @@ -692,6 +693,105 @@ const configTranslationCommandHandlers: CommandHandlerTable = { }, }; +async function checkRequestHandler( + appAgentName: string, + systemContext: CommandHandlerContext, + throwIfFailed: boolean = true, +) { + const result = await resolveCommand( + `${appAgentName} request`, + systemContext, + ); + if (result.descriptor === undefined) { + if (throwIfFailed) { + throw new Error( + `AppAgent '${appAgentName}' doesn't have request command handler`, + ); + } + return false; + } + + const args = result.descriptor.parameters?.args; + if (args === undefined) { + if (throwIfFailed) { + throw new Error( + `AppAgent '${appAgentName}' request command handler doesn't accept any parameter for natural language requests`, + ); + } + return false; + } + + const entries = Object.entries(args); + if (entries.length !== 1 || entries[0][1].implicitQuotes !== true) { + if (throwIfFailed) { + throw new Error( + `AppAgent '${appAgentName}' request command handler doesn't accept parameters resembling natural language requests`, + ); + } + return false; + } + return true; +} + +class ConfigRequestCommandHandler implements CommandHandler { + public readonly description = + "Set the agent that handle natural language requests"; + public readonly parameters = { + args: { + appAgentName: { + description: "name of the agent", + }, + }, + } as const; + public async run( + context: ActionContext, + params: ParsedCommandParams, + ) { + const appAgentName = params.args.appAgentName; + const systemContext = context.sessionContext.agentContext; + const current = systemContext.session.getConfig().request; + if (current === appAgentName) { + displayWarn( + `Natural langue request handling agent is already set to '${appAgentName}'`, + context, + ); + return; + } + + await checkRequestHandler(appAgentName, systemContext); + await changeContextConfig({ request: appAgentName }, context); + + displayResult( + `Natural langue request handling agent is set to '${appAgentName}'`, + context, + ); + } + public async getCompletion( + context: SessionContext, + params: PartialParsedCommandParams, + names: string[], + ): Promise { + const completions: string[] = []; + const systemContext = context.agentContext; + for (const name of names) { + if (name === "appAgentName") { + for (const appAgentName of systemContext.agents.getAppAgentNames()) { + if ( + await checkRequestHandler( + appAgentName, + systemContext, + false, + ) + ) { + completions.push(appAgentName); + } + } + } + } + return completions; + } +} + export function getConfigCommandHandlers(): CommandHandlerTable { return { description: "Configuration commands", @@ -700,6 +800,7 @@ export function getConfigCommandHandlers(): CommandHandlerTable { action: new AgentToggleCommandHandler(AgentToggle.Action), command: new AgentToggleCommandHandler(AgentToggle.Command), agent: new AgentToggleCommandHandler(AgentToggle.Agent), + request: new ConfigRequestCommandHandler(), translation: configTranslationCommandHandlers, explainer: { description: "Explainer configuration", diff --git a/ts/packages/dispatcher/src/session/session.ts b/ts/packages/dispatcher/src/session/session.ts index c22261a6..56587691 100644 --- a/ts/packages/dispatcher/src/session/session.ts +++ b/ts/packages/dispatcher/src/session/session.ts @@ -21,6 +21,7 @@ import { } from "../agent/appAgentManager.js"; import { cloneConfig, mergeConfig } from "./options.js"; import { TokenCounter, TokenCounterData } from "aiclient"; +import { DispatcherName } from "../handlers/common/interactiveIO.js"; const debugSession = registerDebug("typeagent:session"); @@ -101,6 +102,7 @@ async function newSessionDir() { } type DispatcherConfig = { + request: string; translation: { enabled: boolean; model: string; @@ -156,6 +158,8 @@ const defaultSessionConfig: SessionConfig = { actions: undefined, commands: undefined, + // default to dispatcher + request: DispatcherName, translation: { enabled: true, model: "", diff --git a/ts/packages/dispatcher/test/basic.spec.ts b/ts/packages/dispatcher/test/basic.spec.ts index 02091c14..01fccc26 100644 --- a/ts/packages/dispatcher/test/basic.spec.ts +++ b/ts/packages/dispatcher/test/basic.spec.ts @@ -5,8 +5,33 @@ import { createNpmAppAgentProvider } from "../src/agent/npmAgentProvider.js"; import { createDispatcher } from "../src/dispatcher/dispatcher.js"; import { fileURLToPath } from "node:url"; import { getBuiltinAppAgentProvider } from "../src/utils/defaultAppProviders.js"; +import { + ClientIO, + IAgentMessage, + nullClientIO, +} from "../src/handlers/common/interactiveIO.js"; -describe("basic", () => { +const testAppAgentProvider = createNpmAppAgentProvider( + { + test: { + name: "test-agent", + path: fileURLToPath( + new URL("../../../agents/test", import.meta.url), + ), + }, + }, + import.meta.url, +); + +function createTestClientIO(data: IAgentMessage[]): ClientIO { + return { + ...nullClientIO, + setDisplay: (message: IAgentMessage) => data.push(message), + appendDisplay: (message: IAgentMessage) => data.push(message), + }; +} + +describe("dispatcher", () => { it("startup and shutdown", async () => { const dispatcher = await createDispatcher("test", { appAgentProviders: [getBuiltinAppAgentProvider()], @@ -14,28 +39,33 @@ describe("basic", () => { await dispatcher.close(); }); it("Custom NPM App Agent Provider", async () => { + const output: IAgentMessage[] = []; const dispatcher = await createDispatcher("test", { - appAgentProviders: [ - createNpmAppAgentProvider( - { - test: { - name: "test-agent", - path: fileURLToPath( - new URL( - "../../../agents/test", - import.meta.url, - ), - ), - }, - }, - import.meta.url, - ), - ], + appAgentProviders: [testAppAgentProvider], + clientIO: createTestClientIO(output), }); - dispatcher.processCommand( + await dispatcher.processCommand( '@action test add --parameters \'{"a": 1, "b": 2}\'', ); - // TODO: check for the output await dispatcher.close(); + + expect(output.length).toBe(2); + expect(output[1].message).toBe("The sum of 1 and 2 is 3"); + }); + it("Alternate request handler", async () => { + const output: IAgentMessage[] = []; + const dispatcher = await createDispatcher("test", { + appAgentProviders: [testAppAgentProvider], + clientIO: createTestClientIO(output), + }); + await dispatcher.processCommand("@config request test"); + await dispatcher.processCommand("test"); + await dispatcher.close(); + + expect(output.length).toBe(2); + expect(output[0].message).toBe( + "Natural langue request handling agent is set to 'test'", + ); + expect(output[1].message).toBe("test"); }); });