Skip to content

Commit

Permalink
exports & updates for VS Code extension
Browse files Browse the repository at this point in the history
  • Loading branch information
nalbion committed Feb 18, 2024
1 parent 434ed08 commit 9f7d555
Show file tree
Hide file tree
Showing 18 changed files with 285 additions and 121 deletions.
64 changes: 56 additions & 8 deletions src/agents/Agent.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
import path from 'path';
import AgentRegistry, { AgentSearchOpts } from './utils/AgentRegistry';
import { WorkflowManager, createWorkflowManager } from './workflow/WorkflowManager';
import { AgentContext, ProgressData, RoutingContext } from './AgentContext';
import { AgentResponse, AgentResponseStatus } from './types/AgentResponse';
import { AgentInputMessage } from './types/AgentMessage';
import { StepRequirement } from './workflow/WorkflowSchema';
import { areRequirementsMet } from './workflow';

import { LlmRequestMessage, LlmResponseMessage, sendChatRequest } from '../llm';
import { ModelManager } from '../models';
import { AgentConfig, ModelConfig } from '../types';
import { ToolManager } from '../tools';
import { SlashCommand } from '../types/AgentsYml';
import { ChatRequestOptions, ChatRequestOptionsWithOptionalModelConfig } from '../types/ChatRequest';
import { logger, normalisePath } from '../utils';
import { getPromptFromFile, logger, normalisePath } from '../utils';
import { normaliseRoutingContext } from '../utils/routingContext';

export const INSTALLATION_COMMAND = 'installation';
Expand Down Expand Up @@ -48,6 +51,7 @@ export default abstract class Agent {
constructor(
protected agentConfig: AgentConfig,
role?: string,
protected promptsDir = `~/.agents/prompts`,
) {
this.name = agentConfig.name;

Expand Down Expand Up @@ -153,6 +157,9 @@ export default abstract class Agent {
// TODO: use fallbacks?
}

// /** Called when a new AgentContext is created, allows an agent to insert it's own context values */
// initialiseContext(_context: AgentContext) {}

/**
* Receive a message from another Agent and respond to it.
* Subclasses should override `processUserRequest()` if they want to be able to use the WorkflowManager.
Expand Down Expand Up @@ -189,19 +196,56 @@ export default abstract class Agent {
* @param context The Agent's context
* @returns A response to the user, or undefined TODO - or AgentResponse?
*/
protected processUserRequest(
protected async processUserRequest(
input: AgentInputMessage,
context: AgentContext,
): Promise<AgentResponse | undefined> | undefined {
return this.workflowManager?.processUserRequest(input, context, this);
): Promise<AgentResponse | undefined> {
let response: AgentResponse | undefined;

let nextStep = this.getNextStepName(input, context);
if (nextStep) {
response = await this.executeStep(nextStep, input, context);
}

if (!response) {
response = await this.workflowManager?.processUserRequest(input, context, this);
}

return response;
}

protected areStepRequirementsSatisified(_stepId: string, _context: AgentContext): boolean {
// TODO: workflowManager?.areStepRequirementsSatisified(input, context);
protected stepRequirements: { [stepName: string]: StepRequirement[] } = {};

protected areStepRequirementsMet(stepName: string, context: AgentContext): boolean {
const requirements = this.stepRequirements[stepName];
if (requirements) {
return areRequirementsMet(context.routing, requirements);
}
return true;
}

protected async executeStep(_stepId: string, _input: AgentInputMessage, _context: AgentContext): Promise<AgentResponse | undefined> {
protected getNextStepName(input: AgentInputMessage, context: AgentContext): string | undefined {
let nextStep = input.command;

if (!nextStep || !this.areStepRequirementsMet(nextStep, context)) {
// TODO: workflowManager?.getNextStepName(input, context);

for (const stepName in this.stepRequirements) {
if (this.areStepRequirementsMet(stepName, context)) {
nextStep = stepName;
break;
}
}
}

return nextStep;
}

protected async executeStep(
_stepName: string,
_input: AgentInputMessage,
_context: AgentContext,
): Promise<AgentResponse | undefined> {
// TODO: return this.workflowManager?.executeStep(input, context);
return undefined;
}
Expand All @@ -214,10 +258,14 @@ export default abstract class Agent {
* If full-stack and current focus is unknown:
* "You should focus on front-end first, build something to present to the client for feedback, validation & iteration.
*/
protected generateSystemPrompt(_input: AgentInputMessage, _context: AgentContext): string | undefined {
protected generateSystemPrompt(_context: AgentContext): string | undefined | Promise<string | undefined> {
return this.agentConfig.prompts?.find((p) => p.name === 'system')?.input as string;
}

protected getPromptFromFile(promptName: string, context: AgentContext): Promise<string | undefined> {
return getPromptFromFile(path.join(this.promptsDir, promptName + '.txt'), context);
}

/**
* @param agent agent `role` or actual Agent instance
*/
Expand Down
4 changes: 2 additions & 2 deletions src/agents/AgentContext.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ describe('AgentContext', () => {
// Given
const existingContext = {
languages: ['javascript', 'python'],
project: ['name'],
project_name: ['name'],
};

const newResponse = {
Expand All @@ -23,8 +23,8 @@ describe('AgentContext', () => {

// Then
expect(agentContext.routing).toEqual({
project_name: ['name'],
languages: ['javascript', 'python', 'rust'],
project: ['name'],
technologies: ['react'],
platforms: ['web', 'mobile'],
});
Expand Down
18 changes: 16 additions & 2 deletions src/agents/AgentContext.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { ToolContext } from '../tools/ToolTypes';
import { get_directory_tree } from '../tools/impl/get_directory_tree';
import { logger } from '../utils';

interface Disposable {
Expand Down Expand Up @@ -84,7 +85,8 @@ export type ProgressData =
command: string;
};

export type RoutingContext = Record<string, string | string[]>;
export type RoutingContextValue = string | string[] | { [key: string]: RoutingContextValue };
export type RoutingContext = Record<string, RoutingContextValue>;

export class AgentContext implements ToolContext {
routing: RoutingContext = {};
Expand All @@ -110,13 +112,25 @@ export class AgentContext implements ToolContext {
return error;
}

public getDirectoryTree(): string {
return get_directory_tree(this.workspaceFolder);
}

mergeRoutingContext(delta: RoutingContext) {
const mergedContext = { ...this.routing };

Object.keys(delta).forEach((key) => {
if (mergedContext.hasOwnProperty(key)) {
// Merge arrays and remove duplicates
mergedContext[key] = Array.from(new Set([...mergedContext[key], ...delta[key]]));
const mergedValue = mergedContext[key];
const deltaValue = delta[key];
if (Array.isArray(mergedValue) && Array.isArray(deltaValue)) {
mergedContext[key] = Array.from(new Set([...mergedValue, ...deltaValue]));
} else if (typeof mergedValue === 'object' && typeof deltaValue === 'object') {
mergedContext[key] = { ...mergedValue, ...deltaValue };
} else {
mergedContext[key] = deltaValue;
}
} else {
// Add new key and value
mergedContext[key] = delta[key];
Expand Down
20 changes: 20 additions & 0 deletions src/agents/AgentFactory.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { Agent, AgentConfig } from '..';

export default class AgentFactory {
private static agentCreators: { [role: string]: (agentConfig: AgentConfig) => Agent } = {};

static registerAgentCreator(role: string, agentCreator: (agentConfig: AgentConfig) => Agent) {
this.agentCreators[role] = agentCreator;
}

static createAgent(agentConfig: AgentConfig): Agent | undefined {
if (agentConfig.routing?.roles?.length) {
const agentCreator = this.agentCreators[agentConfig.routing.roles.sort().join(',')];
if (agentCreator) {
return agentCreator(agentConfig);
}
}

return undefined;
}
}
8 changes: 4 additions & 4 deletions src/agents/adapters/OpenAiAgent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ import { ToolConfig } from '../../tools/ToolConfig';
export default class OpenAiAgent extends Agent {
private tools: { [name: string]: ToolConfig } = {};

constructor(agentConfig: AgentConfig) {
super(agentConfig);
constructor(agentConfig: AgentConfig, role?: string, promptsDir?: string) {
super(agentConfig, role, promptsDir);
}

registerTool(callback: ToolCallback, definition: ToolDefinition) {
Expand All @@ -21,13 +21,13 @@ export default class OpenAiAgent extends Agent {
};
}

override async processUserRequest(input: AgentInputMessage, context: AgentContext): Promise<AgentResponse> {
protected override async processUserRequest(input: AgentInputMessage, context: AgentContext): Promise<AgentResponse> {
let response = await super.processUserRequest(input, context);

if (!response) {
// async sendMessage(input: AgentInputMessage): Promise<AgentResponse> {
console.info('OpenAiAgent.receiveMessage', input.content);
const messages = agentMessageToLlmMessages(input, this.generateSystemPrompt(input, context));
const messages = agentMessageToLlmMessages(input, await this.generateSystemPrompt(context));

const tools = Object.values(this.tools);
const options = createChatRequestOptions(context.cancellation, {
Expand Down
4 changes: 3 additions & 1 deletion src/agents/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ export {
InstallationInstructions,
} from './Agent';
export { type AgentInputMessage, type AgentResponseMessage, agentMessageToLlmMessages } from './types/AgentMessage';
export { type RoutingContext } from './AgentContext';
export { type RoutingContext, RoutingContextValue } from './AgentContext';
export { type AgentResponse } from './types/AgentResponse';
export { default as AgentFactory } from './AgentFactory';
export { default as AgentRegistry } from './utils/AgentRegistry';
export { createAgent } from './utils/createAgent';
export { FollowUp, ToolObjectDefinition, WorkflowDefinition } from './workflow/WorkflowSchema';
11 changes: 10 additions & 1 deletion src/agents/utils/AgentRegistry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,16 @@ export default class AgentRegistry {
const contextItems = context[key];
const agentItems = agentContext[key];

if (agentItems?.some((item) => contextItems.includes(item))) {
if (
agentItems?.some((item) => {
if (Array.isArray(contextItems)) {
return contextItems.includes(item);
} else if (typeof contextItems === 'string') {
return contextItems === item;
}
return false;
})
) {
rank++;
}
});
Expand Down
50 changes: 30 additions & 20 deletions src/agents/utils/createAgent.ts
Original file line number Diff line number Diff line change
@@ -1,36 +1,22 @@
import Agent, { InstallationInstructions } from '../Agent';
import AgentFactory from '../AgentFactory';
import GitClient from '../adapters/GitClient';
import RouterAgent from '../roles/RouterAgent';
import AgentProtocolClient from '../adapters/AgentProtocolClient';
import CliClientAgent from '../adapters/CliClientAgent';
import OpenAiAgent from '../adapters/OpenAiAgent';
import { AgentConfig } from '../../types';
import RouterAgent from '../roles/RouterAgent';
import { AgentConfig, AgentGitConfig } from '../../types';
import { normalisePath } from '../../utils/fileUtils';

export const createAgent = async (agentConfig: AgentConfig): Promise<Agent | undefined> => {
let agent: Agent | undefined;
let installationNotes: InstallationInstructions | undefined;

if (agentConfig.git) {
if (agentConfig.git !== undefined) {
agentConfig.git.baseDir = normalisePath(agentConfig.git.baseDir);
const installationRequired = await pullGitRepo(agentConfig);
if (installationRequired) {
installationNotes = {
message: `The git repo was cloned for ${agentConfig.name}`,
detail:
`The git repo ${agentConfig.git.repo}/tree/${agentConfig.git.branch} was cloned to ${agentConfig.git.baseDir}.\n` +
'You may need to follow any installation instructions in the repo README.md.',
items: [
{
title: 'View README',
url: `${agentConfig.git.repo}/tree/${agentConfig.git.branch}`,
},
{
title: 'Open project',
url: agentConfig.git.baseDir,
},
],
};
installationNotes = createInstallationNotes(agentConfig as GitAgentConfig);
}
}

Expand All @@ -41,7 +27,10 @@ export const createAgent = async (agentConfig: AgentConfig): Promise<Agent | und
} else if (agentConfig.cli?.command) {
agent = new CliClientAgent(agentConfig as AgentConfig & { cli: { command: string } });
} else if (agentConfig.llm_config?.config_list?.length) {
agent = new OpenAiAgent(agentConfig);
agent = AgentFactory.createAgent(agentConfig);
if (!agent) {
agent = new OpenAiAgent(agentConfig);
}
}

if (agent) {
Expand All @@ -68,3 +57,24 @@ const pullGitRepo = async (agentConfig: AgentConfig) => {
const git = new GitClient(baseDir, options);
return await git.init(repo, !!agentConfig.git?.alwaysPull, branch, remote);
};

type GitAgentConfig = AgentConfig & { git: AgentGitConfig };

const createInstallationNotes = (agentConfig: GitAgentConfig) => {
return {
message: `The git repo was cloned for ${agentConfig.name}`,
detail:
`The git repo ${agentConfig.git.repo}/tree/${agentConfig.git.branch} was cloned to ${agentConfig.git.baseDir}.\n` +
'You may need to follow any installation instructions in the repo README.md.',
items: [
{
title: 'View README',
url: `${agentConfig.git.repo}/tree/${agentConfig.git.branch}`,
},
{
title: 'Open project',
url: agentConfig.git.baseDir,
},
],
};
};
2 changes: 2 additions & 0 deletions src/agents/workflow/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { areRequirementsMet } from './stepRequirements';
export { LinkDefinition, StepDefinition, StepProvide, StepRequirement, ToolObjectDefinition } from './WorkflowSchema';
43 changes: 43 additions & 0 deletions src/agents/workflow/stepRequirements.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { RoutingContext } from '..';
import { StepRequirement } from './WorkflowSchema';

export const areRequirementsMet = (context: RoutingContext, requirements?: StepRequirement[]) => {
if (!requirements) {
return true;
}

return requirements.every((requirement) => {
// is the required key even in the context?
if (!(requirement.name in context)) {
if (requirement.condition === 'undefined') {
return true;
}
return false;
}

const contextValue = context[requirement.name];

if (requirement.condition) {
if (requirement.condition === 'undefined') {
return contextValue === undefined;
}
if (typeof requirement.condition === 'number') {
return Array.isArray(contextValue) || typeof contextValue === 'string'
? contextValue.length >= requirement.condition
: false;
}
if (Array.isArray(contextValue)) {
return contextValue.includes(requirement.condition);
}
if (
typeof contextValue === 'string' &&
requirement.condition.startsWith('/') &&
requirement.condition.endsWith('/')
) {
return new RegExp(requirement.condition).test(contextValue);
}
}

return !!contextValue;
});
};
Loading

0 comments on commit 9f7d555

Please sign in to comment.