Skip to content

Commit

Permalink
various updates
Browse files Browse the repository at this point in the history
  • Loading branch information
danielcampagnolitg committed Sep 30, 2024
1 parent 9737fa9 commit 9bbbfc7
Show file tree
Hide file tree
Showing 36 changed files with 555 additions and 238 deletions.
2 changes: 1 addition & 1 deletion frontend/src/app/shell/shell.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@
</a>
<mat-divider></mat-divider>
<a mat-list-item routerLink="/code" routerLinkActive="active" (click)="isMobile && sidenav.close()">
<span translate>Code Operations</span>
<span translate>Code Repositories</span>
</a>
<mat-divider></mat-divider>
</mat-list>
Expand Down
5 changes: 4 additions & 1 deletion src/agent/LlmFunctions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@ import { FUNC_SEP, FunctionSchema, getFunctionSchemas } from '#functionSchema/fu
import { FunctionCall } from '#llm/llm';
import { logger } from '#o11y/logger';

import { FileSystem } from '#functions/storage/filesystem';
import { FileSystemService } from '#functions/storage/fileSystemService';
import { GetToolType, ToolType, toolType } from '#functions/toolType';

import { functionFactory } from '#functionSchema/functionDecorators';
import { FileSystemRead } from '#functions/storage/FileSystemRead';

/**
* Holds the instances of the classes with function callable methods.
Expand All @@ -31,6 +32,8 @@ export class LlmFunctions {
for (const functionClassName of functionClassNames) {
const ctor = functionFactory()[functionClassName];
if (ctor) this.functionInstances[functionClassName] = new ctor();
else if (functionClassName === 'FileSystem')
this.functionInstances[FileSystemRead.name] = new FileSystemRead(); // backwards compatability from creating FileSystemRead/Write wrappers
else logger.warn(`${functionClassName} not found`);
}
return this;
Expand Down
13 changes: 6 additions & 7 deletions src/agent/agentContext.test.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { expect } from 'chai';
import sinon from 'sinon';
import { LlmFunctions } from '#agent/LlmFunctions';
import { createContext, deserializeAgentContext, serializeContext } from '#agent/agentContextLocalStorage';
import { createContext } from '#agent/agentContextLocalStorage';
import { AgentContext } from '#agent/agentContextTypes';
import { RunAgentConfig } from '#agent/agentRunner';
import { FileSystem } from '#functions/storage/filesystem';
import { deserializeAgentContext, serializeContext } from '#agent/agentSerialization';
import { FileSystemRead } from '#functions/storage/FileSystemRead';
import { FileSystemService } from '#functions/storage/fileSystemService';
import { LlmTools } from '#functions/util';
import { GPT4o } from '#llm/models/openai';
import { appContext } from '../app';
Expand All @@ -25,7 +27,7 @@ describe('agentContext', () => {
xhard: GPT4o(),
};
// We want to check that the FileSystem gets re-added by the resetFileSystemFunction function
const functions = new LlmFunctions(LlmTools, FileSystem);
const functions = new LlmFunctions(LlmTools, FileSystemRead);

const config: RunAgentConfig = {
agentName: 'SWE',
Expand All @@ -36,7 +38,7 @@ describe('agentContext', () => {
metadata: { 'metadata-key': 'metadata-value' },
};
const agentContext: AgentContext = createContext(config);
agentContext.fileSystem.setWorkingDirectory('./workingDir');
agentContext.fileSystem.setWorkingDirectory('./src');
agentContext.memory.memory_key = 'memory_value';
agentContext.functionCallHistory.push({
function_name: 'func',
Expand All @@ -63,9 +65,6 @@ describe('agentContext', () => {
const reserialised = serializeContext(deserialised);

expect(serialized).to.be.deep.equal(reserialised);

// test agentContext.resetFileSystemFunction()
expect(deserialised.fileSystem === deserialised.functions.getFunctionInstanceMap()[FileSystem.name]).to.be.true;
});
});
});
106 changes: 4 additions & 102 deletions src/agent/agentContextLocalStorage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,9 @@ import { LlmFunctions } from '#agent/LlmFunctions';
import { ConsoleCompletedHandler } from '#agent/agentCompletion';
import { AgentContext, AgentLLMs } from '#agent/agentContextTypes';
import { RunAgentConfig } from '#agent/agentRunner';
import { getCompletedHandler } from '#agent/completionHandlerRegistry';
import { FileSystem } from '#functions/storage/filesystem';
import { deserializeLLMs } from '#llm/llmFactory';
import { FileSystemService } from '#functions/storage/fileSystemService';
import { logger } from '#o11y/logger';
import { currentUser } from '#user/userService/userContext';
import { appContext } from '../app';

export const agentContextStorage = new AsyncLocalStorage<AgentContext>();

Expand Down Expand Up @@ -44,30 +41,15 @@ export function addNote(note: string): void {
/**
* @return the filesystem on the current agent context
*/
export function getFileSystem(): FileSystem {
if (!agentContextStorage.getStore()) return new FileSystem();
export function getFileSystem(): FileSystemService {
if (!agentContextStorage.getStore()) return new FileSystemService();
const filesystem = agentContextStorage.getStore()?.fileSystem;
if (!filesystem) throw new Error('No file system available on the agent context');
return filesystem;
}

/**
* After we have added functions or deserialized an agent, we need to make sure that if the
* agent has the FileSystem function available that it's the same object as the FileSystem on the agent context
* @param agent
*/
function resetFileSystemFunction(agent: AgentContext) {
// Make sure we have the same FileSystem object on the context and in the functions
const functions: LlmFunctions = Array.isArray(agent.functions) ? new LlmFunctions(...agent.functions) : agent.functions;
if (functions.getFunctionClassNames().includes(FileSystem.name)) {
functions.removeFunctionClass(FileSystem.name);
functions.addFunctionInstance(agent.fileSystem, FileSystem.name);
}
agent.functions = functions;
}

export function createContext(config: RunAgentConfig): AgentContext {
const fileSystem = new FileSystem(config.fileSystemPath);
const fileSystem = new FileSystemService(config.fileSystemPath);
const hilBudget = config.humanInLoop?.budget ?? (process.env.HIL_BUDGET ? parseFloat(process.env.HIL_BUDGET) : 2);
const context: AgentContext = {
agentId: config.resumeAgentId || randomUUID(),
Expand Down Expand Up @@ -98,85 +80,5 @@ export function createContext(config: RunAgentConfig): AgentContext {
invoking: [],
lastUpdate: Date.now(),
};
resetFileSystemFunction(context);
return context;
}

export function serializeContext(context: AgentContext): Record<string, any> {
const serialized = {};

for (const key of Object.keys(context) as Array<keyof AgentContext>) {
if (context[key] === undefined) {
// do nothing
} else if (context[key] === null) {
serialized[key] = null;
}
// Copy primitive properties across
else if (typeof context[key] === 'string' || typeof context[key] === 'number' || typeof context[key] === 'boolean') {
serialized[key] = context[key];
}
// Assume arrays (functionCallHistory) can be directly de(serialised) to JSON
else if (Array.isArray(context[key])) {
serialized[key] = context[key];
}
// Object type check for a toJSON function
else if (typeof context[key] === 'object' && context[key].toJSON) {
serialized[key] = context[key].toJSON();
}
// Handle Maps (must only contain primitive/simple object values)
else if (key === 'memory' || key === 'metadata') {
serialized[key] = context[key];
} else if (key === 'llms') {
serialized[key] = {
easy: context.llms.easy?.getId(),
medium: context.llms.medium?.getId(),
hard: context.llms.hard?.getId(),
xhard: context.llms.xhard?.getId(),
};
} else if (key === 'user') {
serialized[key] = context.user.id;
} else if (key === 'completedHandler') {
context.completedHandler.agentCompletedHandlerId();
}
// otherwise throw error
else {
throw new Error(`Cant serialize context property ${key}`);
}
}
return serialized;
}

export async function deserializeAgentContext(serialized: Record<keyof AgentContext, any>): Promise<AgentContext> {
const context: Partial<AgentContext> = {};

for (const key of Object.keys(serialized)) {
// copy Array and primitive properties across
if (Array.isArray(serialized[key]) || typeof serialized[key] === 'string' || typeof serialized[key] === 'number' || typeof serialized[key] === 'boolean') {
context[key] = serialized[key];
}
}

context.fileSystem = new FileSystem().fromJSON(serialized.fileSystem);
context.functions = new LlmFunctions().fromJSON(serialized.functions ?? (serialized as any).toolbox); // toolbox for backward compat

resetFileSystemFunction(context as AgentContext); // TODO add a test for this

context.memory = serialized.memory;
context.metadata = serialized.metadata;
context.llms = deserializeLLMs(serialized.llms);

const user = currentUser();
if (serialized.user === user.id) context.user = user;
else context.user = await appContext().userService.getUser(serialized.user);

context.completedHandler = getCompletedHandler(serialized.completedHandler);

// backwards compatability
if (!context.type) context.type = 'xml';
if (!context.iterations) context.iterations = 0;

// Need to default empty parameters. Seems to get lost in Firestore
for (const call of context.functionCallHistory) call.parameters ??= {};

return context as AgentContext;
}
4 changes: 2 additions & 2 deletions src/agent/agentContextTypes.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { LlmFunctions } from '#agent/LlmFunctions';
import { FileSystem } from '#functions/storage/filesystem';
import { FileSystemService } from '#functions/storage/fileSystemService';
import { FunctionCall, FunctionCallResult, LLM, LlmMessage } from '#llm/llm';
import { User } from '#user/user';

Expand Down Expand Up @@ -95,7 +95,7 @@ export interface AgentContext {
/** Pre-configured LLMs by task difficulty level for the agent. Specific LLMs can always be instantiated if required. */
llms: AgentLLMs;
/** Working filesystem */
fileSystem?: FileSystem | null;
fileSystem?: FileSystemService | null;
/** Memory persisted over the agent's executions */
memory: Record<string, string>;
/** Time of the last database write of the state */
Expand Down
4 changes: 2 additions & 2 deletions src/agent/agentPromptUtils.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { agentContext, getFileSystem } from '#agent/agentContextLocalStorage';
import { FileSystemService } from '#functions/storage/fileSystemService';
import { FileMetadata, FileStore } from '#functions/storage/filestore';
import { FileSystem } from '#functions/storage/filesystem';
import { FunctionCallResult } from '#llm/llm';
import { logger } from '#o11y/logger';

Expand Down Expand Up @@ -29,7 +29,7 @@ export async function buildToolStatePrompt(): Promise<string> {
*/
function buildFileSystemPrompt(): string {
const functions = agentContext().functions;
if (!functions.getFunctionClassNames().includes(FileSystem.name)) return '';
if (!functions.getFunctionClassNames().includes(FileSystemService.name)) return '';
const fileSystem = getFileSystem();
return `\n<file_system>
<base_path>${fileSystem.basePath}</base_path>
Expand Down
84 changes: 84 additions & 0 deletions src/agent/agentSerialization.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { LlmFunctions } from '#agent/LlmFunctions';
import { AgentContext } from '#agent/agentContextTypes';
import { getCompletedHandler } from '#agent/completionHandlerRegistry';
import { FileSystemService } from '#functions/storage/fileSystemService';
import { deserializeLLMs } from '#llm/llmFactory';
import { currentUser } from '#user/userService/userContext';
import { appContext } from '../app';

export function serializeContext(context: AgentContext): Record<string, any> {
const serialized = {};

for (const key of Object.keys(context) as Array<keyof AgentContext>) {
if (context[key] === undefined) {
// do nothing
} else if (context[key] === null) {
serialized[key] = null;
}
// Copy primitive properties across
else if (typeof context[key] === 'string' || typeof context[key] === 'number' || typeof context[key] === 'boolean') {
serialized[key] = context[key];
}
// Assume arrays (functionCallHistory) can be directly de(serialised) to JSON
else if (Array.isArray(context[key])) {
serialized[key] = context[key];
}
// Object type check for a toJSON function
else if (typeof context[key] === 'object' && context[key].toJSON) {
serialized[key] = context[key].toJSON();
}
// Handle Maps (must only contain primitive/simple object values)
else if (key === 'memory' || key === 'metadata') {
serialized[key] = context[key];
} else if (key === 'llms') {
serialized[key] = {
easy: context.llms.easy?.getId(),
medium: context.llms.medium?.getId(),
hard: context.llms.hard?.getId(),
xhard: context.llms.xhard?.getId(),
};
} else if (key === 'user') {
serialized[key] = context.user.id;
} else if (key === 'completedHandler') {
context.completedHandler.agentCompletedHandlerId();
}
// otherwise throw error
else {
throw new Error(`Cant serialize context property ${key}`);
}
}
return serialized;
}

export async function deserializeAgentContext(serialized: Record<keyof AgentContext, any>): Promise<AgentContext> {
const context: Partial<AgentContext> = {};

for (const key of Object.keys(serialized)) {
// copy Array and primitive properties across
if (Array.isArray(serialized[key]) || typeof serialized[key] === 'string' || typeof serialized[key] === 'number' || typeof serialized[key] === 'boolean') {
context[key] = serialized[key];
}
}

context.fileSystem = new FileSystemService().fromJSON(serialized.fileSystem);
context.functions = new LlmFunctions().fromJSON(serialized.functions ?? (serialized as any).toolbox); // toolbox for backward compat

context.memory = serialized.memory;
context.metadata = serialized.metadata;
context.llms = deserializeLLMs(serialized.llms);

const user = currentUser();
if (serialized.user === user.id) context.user = user;
else context.user = await appContext().userService.getUser(serialized.user);

context.completedHandler = getCompletedHandler(serialized.completedHandler);

// backwards compatability
if (!context.type) context.type = 'xml';
if (!context.iterations) context.iterations = 0;

// Need to default empty parameters. Seems to get lost in Firestore
for (const call of context.functionCallHistory) call.parameters ??= {};

return context as AgentContext;
}
2 changes: 1 addition & 1 deletion src/agent/agentStateService/fileAgentStateService.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { mkdirSync, readFileSync, readdirSync, writeFileSync } from 'fs';
import { unlinkSync } from 'node:fs';
import { LlmFunctions } from '#agent/LlmFunctions';
import { deserializeAgentContext, serializeContext } from '#agent/agentContextLocalStorage';
import { AgentContext, AgentRunningState } from '#agent/agentContextTypes';
import { deserializeAgentContext, serializeContext } from '#agent/agentSerialization';
import { AgentStateService } from '#agent/agentStateService/agentStateService';
import { functionFactory } from '#functionSchema/functionDecorators';
import { logger } from '#o11y/logger';
Expand Down
2 changes: 1 addition & 1 deletion src/agent/agentStateService/inMemoryAgentStateService.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { LlmFunctions } from '#agent/LlmFunctions';
import { deserializeAgentContext, serializeContext } from '#agent/agentContextLocalStorage';
import { AgentContext, AgentRunningState } from '#agent/agentContextTypes';
import { deserializeAgentContext, serializeContext } from '#agent/agentSerialization';
import { AgentStateService } from '#agent/agentStateService/agentStateService';
import { functionFactory } from '#functionSchema/functionDecorators';
import { logger } from '#o11y/logger';
Expand Down
6 changes: 3 additions & 3 deletions src/agent/cachingPythonAgentRunner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { AgentContext } from '#agent/agentContextTypes';
import { AGENT_COMPLETED_NAME, AGENT_REQUEST_FEEDBACK, AGENT_SAVE_MEMORY_CONTENT_PARAM_NAME } from '#agent/agentFunctions';
import { buildFunctionCallHistoryPrompt, buildMemoryPrompt, buildToolStatePrompt, updateFunctionSchemas } from '#agent/agentPromptUtils';
import { AgentExecution, formatFunctionError, formatFunctionResult } from '#agent/agentRunner';
import { agentHumanInTheLoop, notifySupervisor } from '#agent/humanInTheLoop';
import { humanInTheLoop, notifySupervisor } from '#agent/humanInTheLoop';
import { convertJsonToPythonDeclaration, extractPythonCode } from '#agent/pythonAgentUtils';
import { getServiceName } from '#fastify/trace-init/trace-init';
import { FUNC_SEP, FunctionSchema, getAllFunctionSchemas } from '#functionSchema/functions';
Expand Down Expand Up @@ -105,7 +105,7 @@ export async function runCachingPythonAgent(agent: AgentContext): Promise<AgentE
try {
// Human in the loop checks ------------------------
if (hilCount && countSinceHil === hilCount) {
await agentHumanInTheLoop(`Agent control loop has performed ${hilCount} iterations`);
await humanInTheLoop(`Agent control loop has performed ${hilCount} iterations`);
countSinceHil = 0;
}
countSinceHil++;
Expand All @@ -114,7 +114,7 @@ export async function runCachingPythonAgent(agent: AgentContext): Promise<AgentE
if (hilBudget && agent.budgetRemaining <= 0) {
// HITL happens once budget is exceeded, which may be more than the allocated budget
const increase = agent.hilBudget - agent.budgetRemaining;
await agentHumanInTheLoop(`Agent cost has increased by USD\$${increase.toFixed(2)}. Increase budget by $${agent.hilBudget}`);
await humanInTheLoop(`Agent cost has increased by USD\$${increase.toFixed(2)}. Increase budget by $${agent.hilBudget}`);
agent.budgetRemaining = agent.hilBudget;
}

Expand Down
6 changes: 1 addition & 5 deletions src/agent/humanInTheLoop.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,10 +53,6 @@ export async function notifySupervisor(agent: AgentContext, message: string) {
}
}

export async function agentHumanInTheLoop(reason: string) {
await waitForConsoleInput(reason);
}

export async function toolHumanInTheLoop(reason: string) {
export async function humanInTheLoop(reason: string) {
await waitForConsoleInput(reason);
}
Loading

0 comments on commit 9bbbfc7

Please sign in to comment.