Skip to content

Commit

Permalink
working on tools
Browse files Browse the repository at this point in the history
  • Loading branch information
nalbion committed Feb 16, 2024
1 parent 63775a8 commit f2d8abf
Show file tree
Hide file tree
Showing 23 changed files with 363 additions and 63 deletions.
2 changes: 1 addition & 1 deletion src/agents/Agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -248,7 +248,7 @@ export default abstract class Agent {
}

logger.info(`Agent ${this.name} executeInternalTool ${name} with parameters:`, parameters);
tool.execute(context, ...parameters);
return tool.execute(context, ...parameters);
}

addRole(role: string, agentConfig: AgentConfig) {
Expand Down
3 changes: 2 additions & 1 deletion src/agents/AgentContext.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { ToolContext } from '../tools/ToolTypes';
import { logger } from '../utils';

interface Disposable {
Expand Down Expand Up @@ -85,7 +86,7 @@ export type ProgressData =

export type RoutingContext = Record<string, string | string[]>;

export class AgentContext {
export class AgentContext implements ToolContext {
routing: RoutingContext = {};

constructor(
Expand Down
8 changes: 6 additions & 2 deletions src/agents/adapters/AgentProtocolClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { AgentResponse, AgentResponseStatus, AgentInputMessage } from '../types'
import { AgentConfig } from '../../types';
import { ToolManager } from '../../tools/ToolManager';
import { TOOL_EXECUTE_PYTHON_CODE, TOOL_WRITE_FILE } from '../../tools/impl';
import { TOOL_EXECUTE_SHELL } from '../../utils/dockerUtils';
import { TOOL_EXECUTE_SHELL } from '../../tools/impl/execute_shell';

export default class AgentProtocolClient extends Agent {
private hostUrl: string;
Expand Down Expand Up @@ -36,7 +36,11 @@ export default class AgentProtocolClient extends Agent {
const { command } = step.additional_output || {};
if (command) {
// TODO: ask_user(question: string)
ToolManager.executeTool(command.name, context, command.args);
const toolResult = await ToolManager.executeTool(command.name, context, command.args);
context.onProgress({
type: 'content',
content: `Executed tool: ${command.name}(${command.args}) with result: ${toolResult}`,
});
}

// status: 'completed'
Expand Down
10 changes: 5 additions & 5 deletions src/agents/adapters/OpenAiAgent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@ import Agent from '../Agent';
import { AgentContext } from '../AgentContext';
import { AgentInputMessage, AgentResponse, AgentResponseStatus, agentMessageToLlmMessages } from '../types';
import { ChatCompletionTool } from '../../llm';
import { Tool, ToolDefinition, ToolConfig, ToolCallback } from '../../tools';
import { Tool, ToolDefinition, ToolCallback } from '../../tools';
import { AgentConfig } from '../../types';
import { createChatRequestOptions } from '../../types/ChatRequest';
import { ChatCompletionMessageToolCall } from '../../llm/message';
import { ToolConfig } from '../../tools/ToolConfig';

export default class OpenAiAgent extends Agent {
private tools: { [name: string]: ToolConfig } = {};
Expand Down Expand Up @@ -49,9 +49,9 @@ export default class OpenAiAgent extends Agent {
} else {
const { tools } = llmResponse;
const results = await Promise.all(
tools.map((tool: ChatCompletionMessageToolCall) => {
this.tools[tool.function.name].implementation.execute(context, JSON.parse(tool.function.arguments));
}),
tools.map((tool) =>
this.tools[tool.function.name].implementation.execute(context, JSON.parse(tool.function.arguments)),
),
);

response = {
Expand Down
22 changes: 16 additions & 6 deletions src/agents/adapters/agentprotocol/agentprotocol.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
import Agent, { type StepHandler, type StepInput, type StepResult, type TaskInput, Artifact } from 'agent-protocol';
import { type Request, type Response } from 'express';
import Agent, {
type StepHandler,
type StepInput,
type StepResult,
type TaskInput,
type Artifact,
} from 'agent-protocol';
import AgentProtocolServer from '../AgentProtocolServer';
import { logger } from '../../../utils/Logger';
import { AgentResponseStatus } from '../../types/AgentResponse';
import { FileStorage } from 'any-cloud-storage';
import { type FileStorage } from 'any-cloud-storage';

/**
* Handles incoming requests: POST /ap/v1/agent/task
Expand Down Expand Up @@ -38,7 +45,10 @@ export const startServer = (config: { port?: number; workspace?: string }) => {
apAgent.start();
};

type ServerRequestHandler<R, S> = (request: R, response: S) => void | Promise<void>;
type ServerRequestHandler<R extends Request = Request, S extends Response = Response> = (
request: R,
response: S,
) => void | Promise<void>;
type BuildServerConfig = {
workspace?: string;
// Waiting on https://github.com/AI-Engineer-Foundation/agent-protocol/pull/100
Expand All @@ -48,17 +58,17 @@ type BuildServerConfig = {
const buildServer = (config: BuildServerConfig) => {
apAgent = Agent.handleTask(taskHandler, config);
// Waiting on https://github.com/AI-Engineer-Foundation/agent-protocol/pull/99
const apHandler = (apAgent as any).build() as ServerRequestHandler<R, S>; // the Express app
const apHandler = (apAgent as any).build() as ServerRequestHandler; // the Express app
apAgent.start();
return apHandler;
};

export const createServerless = <R = Request, S = Response>(
export const createServerless = <R extends Request = Request, S extends Response = Response>(
config: BuildServerConfig,
promisedStorage?: Promise<FileStorage>,
) => {
if (promisedStorage) {
const promisedHandler = promisedStorage.then((storage) => {
const promisedHandler = promisedStorage.then((_storage) => {
// config.artifactStorage = ArtifactStorageFactory.create(config.artifactStorage);
// config.artifactStorage = new AnyCloudArtifactStorage(storage);
return buildServer(config);
Expand Down
3 changes: 2 additions & 1 deletion src/agents/adapters/agentprotocol/firebase.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { type Response } from 'express';
import { Request, onRequest } from 'firebase-functions/v2/https';
import { createServerless } from './agentprotocol';
import { useAnyCloudStorage } from '../../../utils/fileStorage';

const promisedStorage = useAnyCloudStorage({ type: 'firebase', bucket: 'my-bucket' });

const app = createServerless<Request, Express.Response>(
const app = createServerless<Request, Response>(
{
// artifactStorage: new AnyCloudArtifactStorage(storage),
},
Expand Down
2 changes: 1 addition & 1 deletion src/agents/workflow/WorkflowSchema.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ToolDefinition } from '../../tools/ToolDefinition';
import { ToolDefinition } from '../../tools/ToolTypes';
import { ModelSettings } from '../../types/AIConfig';
import { SlashCommand } from '../../types/AgentsYml';

Expand Down
2 changes: 1 addition & 1 deletion src/llm/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
export { sendChatRequest } from './sendChatRequest';
export { LlmMessage, LlmRequestMessage, LlmResponseMessage } from './message';
export { ChatCompletionTool, FunctionDefinition } from './tools';
export { ChatCompletionTool } from './tools';
10 changes: 3 additions & 7 deletions src/llm/tools.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,10 @@
import { ToolDefinition } from '../tools';

export interface ChatCompletionTool {
type: 'function';
function: FunctionDefinition;
function: ToolDefinition;
}

export type FunctionDefinition = {
name: string;
description: string;
parameters: Record<string, unknown>;
};

export type ChatCompletionToolChoiceOption =
| 'none'
| 'auto'
Expand Down
3 changes: 1 addition & 2 deletions src/tools/Tool.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { ToolCallback } from './tool_types';
import { ChatCompletionTool } from '../llm/tools';
import { ToolDefinition } from './ToolDefinition';
import { ToolCallback, ToolDefinition } from './ToolTypes';

export class Tool {
id: string;
Expand Down
7 changes: 4 additions & 3 deletions src/tools/ToolConfig.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { FunctionDefinition } from '../llm';
import { Tool } from './Tool';
import { type Tool } from './Tool';
import { ToolDefinition } from './ToolTypes';

export type ToolConfig = {
definition: FunctionDefinition;
// As per Open AI function calling / Tool definition
definition: ToolDefinition;
implementation: Tool;
};
5 changes: 0 additions & 5 deletions src/tools/ToolDefinition.ts

This file was deleted.

4 changes: 2 additions & 2 deletions src/tools/ToolManager.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { ToolManager } from './ToolManager';
import { ToolCallback, ToolContext } from './tool_types';
import { ToolCallback, ToolContext } from './ToolTypes';
import { AgentContext } from '../agents/AgentContext';
import { ToolDefinition } from './ToolDefinition';
import { ToolDefinition } from './ToolTypes';

describe('ToolManager', () => {
let toolCallback: ToolCallback;
Expand Down
5 changes: 3 additions & 2 deletions src/tools/ToolManager.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { logger } from '../utils';
import { Tool } from './Tool';
import { ToolDefinition } from './ToolDefinition';
import { ToolCallback, ToolConfig, ToolContext } from './tool_types';
import { ToolDefinition } from './ToolTypes';
import { ToolCallback, ToolContext } from './ToolTypes';
import { ToolConfig } from './ToolConfig';

export class ToolManager {
private static tools: Record<string, ToolConfig> = {};
Expand Down
21 changes: 21 additions & 0 deletions src/tools/ToolTypes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { type ProgressData, type CancellationToken, type RoutingContext } from '../agents/AgentContext';

export type ToolContext = {
workspaceFolder: string;
routing: RoutingContext;
askForUserPermission: (message: string) => Promise<boolean>;
cancellation: CancellationToken;
onProgress: (progressData: ProgressData) => void;
formatError: (error: string) => string;
mergeRoutingContext: (delta: RoutingContext) => RoutingContext;
};

export type ToolCallback = (
context: ToolContext,
...parameters: any
) => string | undefined | void | Promise<string | undefined | void>;
export type ToolDefinition = {
name: string;
description: string;
parameters: Record<string, unknown>;
};
38 changes: 38 additions & 0 deletions src/tools/impl/clone_repo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// import { ToolManager } from '../ToolManager';
// import { ToolContext } from '../ToolTypes';

// export const TOOL_CLONE_REPO = 'clone_repo';

// /**
// * Write text to a file
// * @param filename The name of the file to write
// * @param contents The contents of the file to write
// */
// const clone_repo = async (context: ToolContext, repoUrl: string, localPath: string) => {
// try {
// const fullPath = await context.git.clone(repoUrl, localPath);
// const project = localPath.split('/').pop();
// return `The repository ${repoUrl} has been cloned locally as project "${project}" to ${localPath}`;
// } catch (err) {
// return `Error cloning repository from ${repoUrl} to ${localPath}: ${(err as Error).message}`;
// }
// };

// ToolManager.registerTool(clone_repo, {
// name: TOOL_CLONE_REPO,
// description: 'Clone a git repository',
// parameters: {
// type: 'object',
// properties: {
// repository_url: {
// type: 'string',
// description: 'The URL of the repository to clone',
// },
// local_path: {
// type: 'string',
// description: 'The local path to clone the repository to',
// },
// },
// required: ['repository_url', 'local_path'],
// },
// });
10 changes: 5 additions & 5 deletions src/tools/impl/execute_python_code.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { ToolManager } from '../ToolManager';
import { ToolContext } from '../tool_types';
import { ToolContext } from '../ToolTypes';
import { logger } from '../../utils/Logger';

export const TOOL_EXECUTE_PYTHON_CODE = 'execute_python_code';
Expand All @@ -8,8 +8,8 @@ export const TOOL_EXECUTE_PYTHON_FILE = 'execute_python_file';
import path from 'path';
import { runInDocker } from '../../utils/dockerUtils';

const runPythonInDocker = async (context: ToolContext, ...args: string[]) => {
return runInDocker(context, 'python:latest', 'python', ...args);
const runPythonInDocker = async (context: ToolContext, args: string[]) => {
return runInDocker(context, 'python:latest', { Cmd: ['python'] }, args);
};

/**
Expand All @@ -19,7 +19,7 @@ const execute_python_code = async (context: ToolContext, code: string) => {
let content;
try {
logger.info('Executing Python code:', code);
const result = await runPythonInDocker(context, '-c', `"${code}"`);
const result = await runPythonInDocker(context, ['-c', `"${code}"`]);
content = 'Executed Python code: \n```' + result + '\n```';

context.onProgress({ type: 'content', content });
Expand Down Expand Up @@ -58,7 +58,7 @@ const executePythonFile = async (context: ToolContext, filename: string, args: s
} else {
// Execute the Python file
try {
content = await runPythonInDocker(context, filename, ...args);
content = await runPythonInDocker(context, [filename, ...args]);
} catch (error) {
content = `Error executing Python file: ${(error as Error).message}`;
}
Expand Down
37 changes: 37 additions & 0 deletions src/tools/impl/execute_shell.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { execSync } from 'child_process';
import { logger } from '../../utils/Logger';
import { ToolContext } from '../ToolTypes';
import { ToolManager } from '../ToolManager';

export const TOOL_EXECUTE_SHELL = 'execute_shell';

export const executeShell = async (
context: ToolContext,
alreadyInDocker: boolean,
args: string[]
): Promise<string> => {
const command = args.join(' ');
if (!alreadyInDocker) {
// TODO: implement allow/deny list as per validate_command in AutoGPT
if (!(await context.askForUserPermission(`Can I run "${command}"?`))) {
return 'Command rejected by user';
}
logger.info(`Running locally: ${command}`);
}
return execSync(command, { cwd: context.workspaceFolder, encoding: 'utf-8' });
};

ToolManager.registerTool(executeShell, {
name: TOOL_EXECUTE_SHELL,
description: 'Execute a Shell Command, non-interactive commands only',
parameters: {
type: 'object',
properties: {
command_line: {
type: 'string',
description: 'The command line to execute',
},
},
required: ['command_line'],
},
});
Loading

0 comments on commit f2d8abf

Please sign in to comment.