Skip to content

Commit

Permalink
feat: Add a basic agent, llm service
Browse files Browse the repository at this point in the history
  • Loading branch information
pranavrajs committed Dec 8, 2024
1 parent 464ff6e commit f5dc916
Show file tree
Hide file tree
Showing 6 changed files with 252 additions and 41 deletions.
178 changes: 178 additions & 0 deletions packages/core/src/agent/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
import { ChatCompletionMessageToolCall } from "openai/resources/index.mjs";
import { LLM } from "../llm";
import { Tool } from "../tool";
import { AgentConfig, AgentResponse, LLMResult, Message, Provider, ToolConfig } from "../types";

export class Agent {
public name: string;
public prompt: string;
public messages: Message[];
public maxIterations: number;
private tools: Tool[];
private llm: any;
private logger: any;
private provider: Provider;
private secrets: Record<string, string>;

constructor(name: string, config: AgentConfig) {
this.name = name;
this.prompt = this.constructPrompt(config);
this.provider = config.provider || 'openai';
this.tools = this.prepareTools(config.tools || []);
this.messages = config.messages || [];
this.maxIterations = config.maxIterations || 10;
this.secrets = config.secrets;
this.logger = config.logger || console;
this.logger.info(this.prompt);

this.llm = new LLM({ provider: this.provider, apiKey: config.secrets.OPENAI_API_KEY, logger: this.logger });
}

public async execute(input: string, context?: string): Promise<string> {
this.setupMessages(input, context);
let iterationCount = 0;
let result: LLMResult = {};

while (true) {
if (iterationCount > this.maxIterations) break;

if (iterationCount === this.maxIterations) {
this.pushToMessages({ role: "system", content: "Provide a final answer" });
}

result = await this.llm.call(this.messages, this.functions());

await this.handleLlmResult(result);

if (result.content?.stop) break;
iterationCount++;
}

return result.content?.output || '';
}

public registerTool(tool: Tool): void {
this.tools.push(tool);
}

private setupMessages(input: string, context?: string): void {
if (!this.messages.length) {
this.pushToMessages({ role: "system", content: this.prompt });

if (context) {
this.pushToMessages({ role: "assistant", content: context });
}
}

this.pushToMessages({ role: "user", content: input });
}

private async handleLlmResult(result: LLMResult): Promise<void> {
if (result.toolCalls) {
const toolResult = await this.executeTool(result.toolCalls);
this.pushToMessages({ role: "assistant", content: toolResult.output || '' });
} else {
this.pushToMessages({
role: "assistant",
content: result.content?.thoughtProcess || result.content?.output || ''
});
}
}

private constructPrompt(config: AgentConfig): string {
if (config.prompt) return config.prompt;

return `
Persona: ${config.persona}
Objective: ${config.goal}
Guidelines:
- Work diligently until the stated objective is achieved.
- Utilize only the provided tools for solving the task. Do not make up names of the functions
- Set 'stop: true' when the objective is complete.
- If you have enough information to provide the details to the user, prepare a final result collecting all the information you have.
Output Structure:
If you find a function, that can be used, directly call the function.
When providing the final answer:
{
'thoughtProcess': 'Describe the reasoning and steps that will lead to the final result.',
'output': 'The complete answer in text form.',
'stop': true
}
`;
}

private prepareTools(tools: ToolConfig[]): Tool[] {
return tools.map(
(tool) => new Tool(tool.name, tool.description, tool.config, tool.implementation)
);
}

private functions(): Array<{
name: string;
description: string;
parameters: {
type: string;
properties: Record<string, { type: string; description: string }>;
required: string[];
};
}> {
return this.tools.map(tool => {
const properties: Record<string, { type: string; description: string }> = {};

Object.entries(tool.config.properties).forEach(([propertyName, propertyDetails]) => {
properties[propertyName] = {
type: propertyDetails.type,
description: propertyDetails.description
};
});

const required = Object.entries(tool.config.properties)
.filter(([_, details]) => details.required)
.map(([name]) => name);

return {
name: tool.name,
description: tool.description,
parameters: {
type: "object",
properties,
required
}
};
});
}

private pushToMessages(message: Message): void {
this.logger.info(`Message: ${JSON.stringify(message)}`);
this.messages.push(message);
}

private async executeTool(toolCalls: ChatCompletionMessageToolCall[]): Promise<AgentResponse> {
const toolCall = toolCalls[0];

const tool = this.tools.find(t => t.name === toolCall.function.name);

if (!tool) {
return { output: "Invalid tool_name, please try again", stop: false };
}

this.logger.info(
`tool_call: ${toolCall.function.name}, ${toolCall.function.arguments}`,
);

const functionArgs = JSON.parse(toolCall.function.arguments);


const toolResult = await tool.execute(
Object.fromEntries(
Object.entries(functionArgs).map(([k, v]) => [k, v])
),
this.secrets
);

return {
output: toolResult || '',
stop: false
};
}
}
12 changes: 6 additions & 6 deletions packages/core/src/llm/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import OpenAI from 'openai';
import { LLMResult, LLMServiceConfig, Provider } from '../types';
import { AgentResponse, LLMResult, LLMServiceConfig, Provider } from '../types';
import { ContentParsingError, InvalidProviderError, LLMModelError, ProviderError } from '../errors';
import { ensureError } from '../utils';

Expand Down Expand Up @@ -76,23 +76,23 @@ export class LLM {
private prepareToolCallResult(message: OpenAI.Chat.ChatCompletionMessage): LLMResult {
return {
toolCalls: message.tool_calls,
content: message.content
content: { output: message.content }
};
}

private prepareContentResult(content: string | null | undefined): LLMResult {
try {
const trimmedContent = content?.trim() ?? "";
const parsed = this.parseJsonContent(trimmedContent);
return { content: parsed.content };
const parsedContent: AgentResponse = this.parseJsonContent(trimmedContent);
return { content: parsedContent };
} catch (error) {
throw new ContentParsingError(
`Failed to prepare content result: ${ensureError(error).message}`
);
}
}

private parseJsonContent(content: string): { content: string } {
private parseJsonContent(content: string): AgentResponse {
try {
return JSON.parse(content);
} catch (error) {
Expand Down Expand Up @@ -158,6 +158,6 @@ export class LLM {
stack: normalizedError.stack,
});

return { content: "An error occurred while processing your request. ${errorMessage}" };
return { content: { output: "An error occurred while processing your request. ${errorMessage}" } };
}
}
35 changes: 11 additions & 24 deletions packages/core/src/tool/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { FunctionInput } from '../types';

describe('Tool', () => {
let validConfig: {
properties?: Record<string, FunctionInput>;
properties: Record<string, FunctionInput>;
secrets?: string[];
};

Expand Down Expand Up @@ -58,33 +58,33 @@ describe('Tool', () => {
);
});

test('throws ExecutionError when no implementation is registered', async () => {
test('throws ExecutionError when no implementation is registered', () => {
const tool = new Tool('testTool', 'Test description', validConfig);

await expect(tool.execute(
expect(() => tool.execute(
{ requiredProp: 'value' },
{ apiKey: 'test-key' }
)).rejects.toThrow(ExecutionError);
)).toThrow(ExecutionError);
});

test('throws InvalidSecretsError when required secrets are missing', async () => {
const implementation = vi.fn();
const tool = new Tool('testTool', 'Test description', validConfig, implementation);

await expect(tool.execute(
expect(() => tool.execute(
{ requiredProp: 'value' },
{}
)).rejects.toThrow(InvalidSecretsError);
)).toThrow(InvalidSecretsError);
});

test('throws InvalidImplementationError when required properties are missing', async () => {
const implementation = vi.fn();
const tool = new Tool('testTool', 'Test description', validConfig, implementation);

await expect(tool.execute(
expect(() => tool.execute(
{ optionalProp: 'value' },
{ apiKey: 'test-key' }
)).rejects.toThrow(InvalidImplementationError);
)).toThrow(InvalidImplementationError);
});

test('handles implementation throwing an error', async () => {
Expand All @@ -93,25 +93,12 @@ describe('Tool', () => {
});
const tool = new Tool('testTool', 'Test description', validConfig, implementation);

await expect(tool.execute(
expect(() => tool.execute(
{ requiredProp: 'value' },
{ apiKey: 'test-key' }
)).rejects.toThrow(ExecutionError);
)).toThrow(ExecutionError);
});

test('works with no properties configured', async () => {
const implementation = vi.fn().mockReturnValue('success');
const tool = new Tool('testTool', 'Test description', {
secrets: ['apiKey']
}, implementation);

const result = await tool.execute(
{},
{ apiKey: 'test-key' }
);

expect(result).toBe('success');
});

test('works with no secrets configured', async () => {
const implementation = vi.fn().mockReturnValue('success');
Expand All @@ -121,7 +108,7 @@ describe('Tool', () => {
}
}, implementation);

const result = await tool.execute(
const result = tool.execute(
{ requiredProp: 'value' }
);

Expand Down
12 changes: 4 additions & 8 deletions packages/core/src/tool/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ export class Tool {
readonly name: string,
readonly description: string,
readonly config: {
properties?: Record<string, FunctionInput>,
properties: Record<string, FunctionInput>,
secrets?: string[],
},
private implementation?: (input: Record<string, unknown>, secrets: Record<string, unknown>) => void
Expand All @@ -20,7 +20,7 @@ export class Tool {
this.implementation = implementation;
}

async execute(input: Record<string, unknown>, providedSecrets: Record<string, unknown> = {}): Promise<unknown> {
execute(input: Record<string, unknown>, providedSecrets: Record<string, string> = {}): any {
this.validateSecrets(providedSecrets);
this.validateInput(input);

Expand All @@ -44,7 +44,7 @@ export class Tool {
}
}

private validateSecrets(providedSecrets: Record<string, unknown>): void {
private validateSecrets(providedSecrets: Record<string, string>): void {
if (!this.config.secrets) {
return;
}
Expand All @@ -55,11 +55,7 @@ export class Tool {
}
}

private validateInput(input: Record<string, unknown>): void {
if (!this.config.properties) {
return;
}

private validateInput(input: Record<string, any>): void {
Object.entries(this.config.properties).forEach(([property, details]) => {
if (details.required && !(property in input)) {
throw new InvalidImplementationError(`Missing required property: ${property}`);
Expand Down
Loading

0 comments on commit f5dc916

Please sign in to comment.