Skip to content

Commit

Permalink
Alternate natural language request handler (microsoft#481)
Browse files Browse the repository at this point in the history
Add command to set a different agent to totally to take over natural
language request, bypassing dispatcher.
The agent implements `request` command handler at the top level with a
single `implicitQuotes` parameter.
User can use the command `@config request <appAgentName>` to switch to
direct natural language request to that agent.

Also, add output validation for dispatcher tests.
  • Loading branch information
curtisman authored Dec 11, 2024
1 parent 6ab98d2 commit bb1f9a3
Show file tree
Hide file tree
Showing 7 changed files with 198 additions and 26 deletions.
2 changes: 1 addition & 1 deletion ts/packages/agents/calendar/src/calendarActionHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ export class CalendarClientLoginCommandHandler
}

const handlers: CommandHandlerTable = {
description: "Calendar login commmand",
description: "Calendar login command",
defaultSubCommand: "login",
commands: {
login: new CalendarClientLoginCommandHandler(),
Expand Down
35 changes: 34 additions & 1 deletion ts/packages/agents/test/src/handler.ts
Original file line number Diff line number Diff line change
@@ -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<void>,
params: ParsedCommandParams<typeof this.parameters>,
) {
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),
};
}

Expand Down
8 changes: 6 additions & 2 deletions ts/packages/dispatcher/src/command/command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down Expand Up @@ -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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -241,7 +241,7 @@ function getLoggerSink(isDbEnabled: () => boolean, clientIO: ClientIO) {
);
}

async function addAppAgentProvidres(
async function addAppAgentProviders(
context: CommandHandlerContext,
appAgentProviders?: AppAgentProvider[],
cacheDirPath?: string,
Expand Down Expand Up @@ -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<string, TypeAgentTranslator>(),
currentScriptDir: process.cwd(),
chatHistory: createChatHistory(),
Expand All @@ -345,7 +345,7 @@ export async function initializeCommandHandlerContext(
batchMode: false,
};

await addAppAgentProvidres(
await addAppAgentProviders(
context,
options?.appAgentProviders,
cacheDirPath,
Expand Down
101 changes: 101 additions & 0 deletions ts/packages/dispatcher/src/handlers/configCommandHandlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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<CommandHandlerContext>,
params: ParsedCommandParams<typeof this.parameters>,
) {
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<CommandHandlerContext>,
params: PartialParsedCommandParams<ParameterDefinitions>,
names: string[],
): Promise<string[]> {
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",
Expand All @@ -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",
Expand Down
4 changes: 4 additions & 0 deletions ts/packages/dispatcher/src/session/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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");

Expand Down Expand Up @@ -101,6 +102,7 @@ async function newSessionDir() {
}

type DispatcherConfig = {
request: string;
translation: {
enabled: boolean;
model: string;
Expand Down Expand Up @@ -156,6 +158,8 @@ const defaultSessionConfig: SessionConfig = {
actions: undefined,
commands: undefined,

// default to dispatcher
request: DispatcherName,
translation: {
enabled: true,
model: "",
Expand Down
68 changes: 49 additions & 19 deletions ts/packages/dispatcher/test/basic.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,37 +5,67 @@ 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()],
});
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");
});
});

0 comments on commit bb1f9a3

Please sign in to comment.