Skip to content

Commit

Permalink
V0.6.0 (TrafficGuard#44)
Browse files Browse the repository at this point in the history
* Display LLM id if not found in the llm map

* Remove LLM.generateFunctionResponse function specific to xml agent

* Update repo doc index building and file selection agent

* Set Gemini 2 Flash costs to be that of 1.5

* Update Anthropic and Deepseek cost calculations for caching

* Store typescript dts files in repo sophia directory

* Add liveFiles properties
  • Loading branch information
danielcampagnolitg authored Jan 15, 2025
1 parent 5a481a1 commit 5a52fe8
Show file tree
Hide file tree
Showing 25 changed files with 1,058 additions and 946 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -236,10 +236,9 @@ export class AgentDetailsComponent implements OnInit {
}

getLlmName(llmId: string): string {
if (!llmId) {
return 'Unknown';
}
return this.llmNameMap.get(llmId) || `Unknown LLM (${llmId})`;
if (!llmId) return 'Unknown';

return this.llmNameMap.get(llmId) || llmId;
}

openFunctionEditModal(): void {
Expand Down
3 changes: 2 additions & 1 deletion src/agent/agentContextLocalStorage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export function llms(): AgentLLMs {
export function addCost(cost: number) {
const store = agentContextStorage.getStore();
if (!store) return;
logger.debug(`Adding cost $${cost}`);
logger.debug(`Adding cost $${cost.toFixed(6)}`);
store.cost += cost;
store.budgetRemaining -= cost;
}
Expand Down Expand Up @@ -81,6 +81,7 @@ export function createContext(config: RunAgentConfig): AgentContext {
memory: {},
invoking: [],
lastUpdate: Date.now(),
liveFiles: [],
};
return context;
}
2 changes: 2 additions & 0 deletions src/agent/agentContextTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,4 +133,6 @@ export interface AgentContext {
functionCallHistory: FunctionCallResult[];
/** How many iterations of the autonomous agent control loop to require human input to continue */
hilCount;
/** Files which are always provided in the agent control loop prompt */
liveFiles: string[];
}
2 changes: 1 addition & 1 deletion src/agent/agentSerialization.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export function serializeContext(context: AgentContext): Record<string, any> {
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
// Assume arrays (functionCallHistory, liveFiles) can be directly de(serialised) to JSON
else if (Array.isArray(context[key])) {
serialized[key] = context[key];
}
Expand Down
37 changes: 37 additions & 0 deletions src/agent/liveFileFunctions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { agentContext } from '#agent/agentContextLocalStorage';
import { func, funcClass } from '#functionSchema/functionDecorators';

export const LIVE_FILES_ADD = 'LiveFiles_addFiles';

export const LIVE_FILES_REMOVE = 'LiveFiles_removeFiles';

/**
* Functions for the agent to add/remove files which always displays the current file contents in the agent control prompt
*/
@funcClass(__filename)
export class LiveFileFunctions {
/**
* Add files which will always have their current contents displayed in the <live-files> section (increasing LLM token costs)
* @param {string[]} files the files to always include the current contents of in the prompt
*/
@func()
async addFiles(files: string[]): Promise<string> {
const agent = agentContext();
agent.liveFiles = Array.from(new Set(...agent.liveFiles, ...files));
return '';
}

/**
* Remove files from the <live-files> section which are no longer required to reduce LLM token costs.
* @param {string[]} files The files to remove
*/
@func()
async removeFiles(files: string[]): Promise<void> {
const agent = agentContext();
const liveFiles = new Set(agent.liveFiles);
for (const f of files) {
liveFiles.delete(f);
}
agent.liveFiles = Array.from(liveFiles);
}
}
21 changes: 18 additions & 3 deletions src/agent/xmlAgentRunner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { humanInTheLoop, notifySupervisor } from '#agent/humanInTheLoop';
import { getServiceName } from '#fastify/trace-init/trace-init';
import { FunctionSchema, getAllFunctionSchemas } from '#functionSchema/functions';
import { FunctionResponse } from '#llm/llm';
import { parseFunctionCallsXml } from '#llm/responseParsers';
import { logger } from '#o11y/logger';
import { withActiveSpan } from '#o11y/trace';
import { envVar } from '#utils/env-var';
Expand Down Expand Up @@ -110,18 +111,27 @@ export async function runXmlAgent(agent: AgentContext): Promise<AgentExecution>
}

let functionResponse: FunctionResponse;
let llmResponse: string;
try {
functionResponse = await agentLLM.generateFunctionResponse(systemPromptWithFunctions, currentPrompt, {
llmResponse = await agentLLM.generateText(systemPromptWithFunctions, currentPrompt, {
id: 'generateFunctionCalls',
stopSequences,
});
functionResponse = {
textResponse: llmResponse,
functions: parseFunctionCallsXml(llmResponse),
};
} catch (e) {
// Should just catch parse error
const retryPrompt = `${currentPrompt}\nNote: Your previous response did not contain the response in the required format of <response><function_calls>...</function_calls></response>. You must reply in the correct response format.`;
functionResponse = await agentLLM.generateFunctionResponse(systemPromptWithFunctions, retryPrompt, {
llmResponse = await agentLLM.generateText(systemPromptWithFunctions, retryPrompt, {
id: 'generateFunctionCalls-retryError',
stopSequences,
});
functionResponse = {
textResponse: llmResponse,
functions: parseFunctionCallsXml(llmResponse),
};
}
currentPrompt = buildFunctionCallHistoryPrompt('history') + buildMemoryPrompt() + filePrompt + userRequestXml + functionResponse.textResponse;
const functionCalls = functionResponse.functions.functionCalls;
Expand All @@ -132,10 +142,15 @@ export async function runXmlAgent(agent: AgentContext): Promise<AgentExecution>
const retryPrompt = `${currentPrompt}
Note: Your previous response did not contain a function call. If you are able to answer/complete the question/task, then call the ${AGENT_COMPLETED_NAME} function with the appropriate response.
If you are unsure what to do next then call the ${AGENT_REQUEST_FEEDBACK} function with a clarifying question.`;
const functionCallResponse: FunctionResponse = await agentLLM.generateFunctionResponse(systemPromptWithFunctions, retryPrompt, {

llmResponse = await agentLLM.generateText(systemPromptWithFunctions, retryPrompt, {
id: 'generateFunctionCalls-retryNoFunctions',
stopSequences,
});
const functionCallResponse: FunctionResponse = {
textResponse: llmResponse,
functions: parseFunctionCallsXml(llmResponse),
};
// retrying
currentPrompt = buildFunctionCallHistoryPrompt('history') + buildMemoryPrompt() + filePrompt + userRequestXml + functionCallResponse.textResponse;
const functionCalls = functionCallResponse.functions.functionCalls;
Expand Down
5 changes: 3 additions & 2 deletions src/functionSchema/functionSchemaParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,10 +85,11 @@ export function functionSchemaParser(sourceFilePath: string): Record<string, Fun
if (jsonUpdatedTimestamp && jsonUpdatedTimestamp > sourceUpdatedTimestamp) {
try {
const json = readFileSync(`${cachedPath}.json`).toString();
logger.debug(`Loading cached function schemas from ${cachedPath}.json`);
if (logger) logger.debug(`Loading cached function schemas from ${cachedPath}.json`);
return JSON.parse(json);
} catch (e) {
logger.info('Error loading cached function schemas: ', e.message);
if (logger) logger.info(`Error loading cached function schemas: ${e.message}`);
else console.log(`Error loading cached function schemas: ${e.message}`);
}
}

Expand Down
67 changes: 48 additions & 19 deletions src/functions/storage/fileSystemService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { VersionControlSystem } from '#functions/scm/versionControlSystem';
import { LlmTools } from '#functions/util';
import { logger } from '#o11y/logger';
import { getActiveSpan, span } from '#o11y/trace';
import { spawnCommand } from '#utils/exec';
import { execCmd, spawnCommand } from '#utils/exec';
import { CDATA_END, CDATA_START, needsCDATA } from '#utils/xml-utils';
import { SOPHIA_FS } from '../../appVars';

Expand Down Expand Up @@ -189,15 +189,20 @@ export class FileSystemService {
* @returns the list of file and folder names
*/
async listFilesInDirectory(dirPath = '.'): Promise<string[]> {
// const rootPath = path.join(this.basePath, dirPath);
const filter: FileFilter = (name) => true;
const ig = ignore();
// TODO should go up the directories to the base path looking for .gitignore files
const gitIgnorePath = path.join(this.getWorkingDirectory(), dirPath, '.gitignore');
// console.log(gitIgnorePath);

// Determine the correct path based on whether dirPath is absolute or relative
let readdirPath: string;
if (path.isAbsolute(dirPath)) {
readdirPath = dirPath;
} else {
readdirPath = path.join(this.getWorkingDirectory(), dirPath);
}

// Load .gitignore rules if present
const gitIgnorePath = path.join(readdirPath, '.gitignore');
if (existsSync(gitIgnorePath)) {
// read the gitignore file into a string array
// console.log(`Found ${gitIgnorePath}`);
let lines = await fs.readFile(gitIgnorePath, 'utf8').then((data) => data.split('\n'));
lines = lines.map((line) => line.trim()).filter((line) => line.length && !line.startsWith('#'), filter);
ig.add(lines);
Expand All @@ -206,17 +211,22 @@ export class FileSystemService {

const files: string[] = [];

const readdirPath = join(this.getWorkingDirectory(), dirPath);
const dirents = await fs.readdir(readdirPath, { withFileTypes: true });
for (const dirent of dirents) {
const direntName = dirent.isDirectory() ? `${dirent.name}/` : dirent.name;
const relativePath = path.relative(this.getWorkingDirectory(), path.join(this.getWorkingDirectory(), dirPath, direntName));
try {
const dirents = await fs.readdir(readdirPath, { withFileTypes: true });
for (const dirent of dirents) {
const direntName = dirent.isDirectory() ? `${dirent.name}/` : dirent.name;
const relativePath = path.relative(this.getWorkingDirectory(), path.join(readdirPath, direntName));

if (!ig.ignores(relativePath)) {
files.push(dirent.name);
if (!ig.ignores(relativePath)) {
files.push(dirent.name);
}
}
} catch (error) {
console.error('Error reading directory:', error);
throw error; // Re-throw the error to be caught by the caller
}
return files; //files.map((file) => file.substring(file.lastIndexOf(path.sep, file.length - 1)));

return files;
}

/**
Expand All @@ -227,7 +237,7 @@ export class FileSystemService {
async listFilesRecursively(dirPath = './', useGitIgnore = true): Promise<string[]> {
this.log.debug(`listFilesRecursively cwd: ${this.workingDirectory}`);

const startPath = path.join(this.getWorkingDirectory(), dirPath);
const startPath = path.isAbsolute(dirPath) ? dirPath : path.join(this.getWorkingDirectory(), dirPath);
// TODO check isnt going higher than this.basePath

const ig = useGitIgnore ? await this.loadGitignoreRules(startPath) : ignore();
Expand Down Expand Up @@ -441,7 +451,9 @@ export class FileSystemService {

async listFolders(dirPath = './'): Promise<string[]> {
const workingDir = this.getWorkingDirectory();
dirPath = path.join(workingDir, dirPath);
if (!path.isAbsolute(dirPath)) {
dirPath = path.join(workingDir, dirPath);
}
try {
const items = await fs.readdir(dirPath);
const folders: string[] = [];
Expand All @@ -450,8 +462,7 @@ export class FileSystemService {
const itemPath = path.join(dirPath, item);
const stat = await fs.stat(itemPath);
if (stat.isDirectory()) {
const relativePath = path.relative(workingDir, itemPath);
folders.push(relativePath);
folders.push(item); // Return only the subfolder name
}
}
return folders;
Expand Down Expand Up @@ -568,6 +579,24 @@ export class FileSystemService {

return tree;
}

async getGitRoot(): Promise<string | null> {
try {
// Use git rev-parse to get the root directory
const result = await execCmd('git rev-parse --show-toplevel');

// If command succeeds, return the trimmed stdout (git root path)
if (!result.error) {
return result.stdout.trim();
}

// If git command fails, return null
return null;
} catch {
// Any unexpected errors also result in null
return null;
}
}
}

/**
Expand Down
34 changes: 12 additions & 22 deletions src/llm/base-llm.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,29 @@
import { StreamTextResult } from 'ai';
import { AgentContext } from '#agent/agentContextTypes';
import { countTokens } from '#llm/tokens';
import { FunctionResponse, GenerateFunctionOptions, GenerateJsonOptions, GenerateTextOptions, LLM, LlmMessage } from './llm';
import { extractJsonResult, extractStringResult, parseFunctionCallsXml } from './responseParsers';
import { GenerateJsonOptions, GenerateTextOptions, LLM, LlmMessage } from './llm';
import { extractJsonResult, extractStringResult } from './responseParsers';

export interface SerializedLLM {
service: string;
model: string;
}

export type InputCostFunction = (input: string, inputTokens: number, usage?: any) => number;
export type OutputCostFunction = (output: string, outputTokens: number) => number;

export function perMilTokens(dollarsPerMillionTokens: number): InputCostFunction {
return (_, tokens) => (tokens * dollarsPerMillionTokens) / 1_000_000;
}

export abstract class BaseLLM implements LLM {
constructor(
protected readonly displayName: string,
protected readonly service: string,
protected readonly model: string,
private maxInputTokens: number,
/** Needed for Aider when we only have the text size */
public readonly calculateInputCost: (input: string) => number,
/** Needed for Aider when we only have the text size */
public readonly calculateOutputCost: (output: string) => number,
protected maxInputTokens: number,
readonly calculateInputCost: InputCostFunction,
readonly calculateOutputCost: OutputCostFunction,
) {}

protected _generateText(systemPrompt: string | undefined, userPrompt: string, opts?: GenerateTextOptions): Promise<string> {
Expand Down Expand Up @@ -85,14 +90,6 @@ export abstract class BaseLLM implements LLM {
return this.generateTextFromMessages(messages, options);
}

async generateFunctionResponse(systemPrompt: string, prompt: string, opts?: GenerateFunctionOptions): Promise<FunctionResponse> {
const response = await this._generateText(systemPrompt, prompt, opts);
return {
textResponse: response,
functions: parseFunctionCallsXml(response),
};
}

generateTextWithResult(userPrompt: string, opts?: GenerateTextOptions): Promise<string>;
generateTextWithResult(systemPrompt: string, userPrompt: string, opts?: GenerateTextOptions): Promise<string>;
generateTextWithResult(messages: LlmMessage[], opts?: GenerateTextOptions): Promise<string>;
Expand Down Expand Up @@ -169,13 +166,6 @@ export abstract class BaseLLM implements LLM {
return this.displayName;
}

calculateCost(input: string, output: string): [totalCost: number, inputCost: number, outputCost: number] {
const inputCost = this.calculateInputCost(input);
const outputCost = this.calculateOutputCost(output);
const totalCost = inputCost + outputCost;
return [totalCost, inputCost, outputCost];
}

countTokens(text: string): Promise<number> {
// defaults to gpt4o token parser
return countTokens(text);
Expand Down
17 changes: 1 addition & 16 deletions src/llm/llm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -146,15 +146,7 @@ export interface LLM {
*/
generateTextWithResult(prompt: string, opts?: GenerateTextOptions): Promise<string>;
generateTextWithResult(systemPrompt: string, prompt: string, opts?: GenerateTextOptions): Promise<string>;
generateTextWithResult<T>(messages: LlmMessage[], opts?: GenerateTextOptions): Promise<T>;

/**
* Generates a response expecting to contain the <function_call> element matching the FunctionResponse type
* @param systemPrompt
* @param userPrompt
* @param opts
*/
generateFunctionResponse(systemPrompt: string, userPrompt: string, opts?: GenerateFunctionOptions): Promise<FunctionResponse>;
generateTextWithResult(messages: LlmMessage[], opts?: GenerateTextOptions): Promise<string>;

/**
* Streams text from the LLM
Expand Down Expand Up @@ -185,13 +177,6 @@ export interface LLM {
/** The maximum number of input tokens */
getMaxInputTokens(): number;

/**
* Calculate costs for generation
* @param input the input text
* @param output the output text
*/
calculateCost(input: string, output: string): [totalCost: number, inputCost: number, outputCost: number];

/**
* @param text
* @returns the number of tokens in the text for this LLM
Expand Down
Loading

0 comments on commit 5a52fe8

Please sign in to comment.