Skip to content

Commit f5dc916

Browse files
committed
feat: Add a basic agent, llm service
1 parent 464ff6e commit f5dc916

File tree

6 files changed

+252
-41
lines changed

6 files changed

+252
-41
lines changed

packages/core/src/agent/index.ts

+178
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
import { ChatCompletionMessageToolCall } from "openai/resources/index.mjs";
2+
import { LLM } from "../llm";
3+
import { Tool } from "../tool";
4+
import { AgentConfig, AgentResponse, LLMResult, Message, Provider, ToolConfig } from "../types";
5+
6+
export class Agent {
7+
public name: string;
8+
public prompt: string;
9+
public messages: Message[];
10+
public maxIterations: number;
11+
private tools: Tool[];
12+
private llm: any;
13+
private logger: any;
14+
private provider: Provider;
15+
private secrets: Record<string, string>;
16+
17+
constructor(name: string, config: AgentConfig) {
18+
this.name = name;
19+
this.prompt = this.constructPrompt(config);
20+
this.provider = config.provider || 'openai';
21+
this.tools = this.prepareTools(config.tools || []);
22+
this.messages = config.messages || [];
23+
this.maxIterations = config.maxIterations || 10;
24+
this.secrets = config.secrets;
25+
this.logger = config.logger || console;
26+
this.logger.info(this.prompt);
27+
28+
this.llm = new LLM({ provider: this.provider, apiKey: config.secrets.OPENAI_API_KEY, logger: this.logger });
29+
}
30+
31+
public async execute(input: string, context?: string): Promise<string> {
32+
this.setupMessages(input, context);
33+
let iterationCount = 0;
34+
let result: LLMResult = {};
35+
36+
while (true) {
37+
if (iterationCount > this.maxIterations) break;
38+
39+
if (iterationCount === this.maxIterations) {
40+
this.pushToMessages({ role: "system", content: "Provide a final answer" });
41+
}
42+
43+
result = await this.llm.call(this.messages, this.functions());
44+
45+
await this.handleLlmResult(result);
46+
47+
if (result.content?.stop) break;
48+
iterationCount++;
49+
}
50+
51+
return result.content?.output || '';
52+
}
53+
54+
public registerTool(tool: Tool): void {
55+
this.tools.push(tool);
56+
}
57+
58+
private setupMessages(input: string, context?: string): void {
59+
if (!this.messages.length) {
60+
this.pushToMessages({ role: "system", content: this.prompt });
61+
62+
if (context) {
63+
this.pushToMessages({ role: "assistant", content: context });
64+
}
65+
}
66+
67+
this.pushToMessages({ role: "user", content: input });
68+
}
69+
70+
private async handleLlmResult(result: LLMResult): Promise<void> {
71+
if (result.toolCalls) {
72+
const toolResult = await this.executeTool(result.toolCalls);
73+
this.pushToMessages({ role: "assistant", content: toolResult.output || '' });
74+
} else {
75+
this.pushToMessages({
76+
role: "assistant",
77+
content: result.content?.thoughtProcess || result.content?.output || ''
78+
});
79+
}
80+
}
81+
82+
private constructPrompt(config: AgentConfig): string {
83+
if (config.prompt) return config.prompt;
84+
85+
return `
86+
Persona: ${config.persona}
87+
Objective: ${config.goal}
88+
Guidelines:
89+
- Work diligently until the stated objective is achieved.
90+
- Utilize only the provided tools for solving the task. Do not make up names of the functions
91+
- Set 'stop: true' when the objective is complete.
92+
- If you have enough information to provide the details to the user, prepare a final result collecting all the information you have.
93+
Output Structure:
94+
If you find a function, that can be used, directly call the function.
95+
When providing the final answer:
96+
{
97+
'thoughtProcess': 'Describe the reasoning and steps that will lead to the final result.',
98+
'output': 'The complete answer in text form.',
99+
'stop': true
100+
}
101+
`;
102+
}
103+
104+
private prepareTools(tools: ToolConfig[]): Tool[] {
105+
return tools.map(
106+
(tool) => new Tool(tool.name, tool.description, tool.config, tool.implementation)
107+
);
108+
}
109+
110+
private functions(): Array<{
111+
name: string;
112+
description: string;
113+
parameters: {
114+
type: string;
115+
properties: Record<string, { type: string; description: string }>;
116+
required: string[];
117+
};
118+
}> {
119+
return this.tools.map(tool => {
120+
const properties: Record<string, { type: string; description: string }> = {};
121+
122+
Object.entries(tool.config.properties).forEach(([propertyName, propertyDetails]) => {
123+
properties[propertyName] = {
124+
type: propertyDetails.type,
125+
description: propertyDetails.description
126+
};
127+
});
128+
129+
const required = Object.entries(tool.config.properties)
130+
.filter(([_, details]) => details.required)
131+
.map(([name]) => name);
132+
133+
return {
134+
name: tool.name,
135+
description: tool.description,
136+
parameters: {
137+
type: "object",
138+
properties,
139+
required
140+
}
141+
};
142+
});
143+
}
144+
145+
private pushToMessages(message: Message): void {
146+
this.logger.info(`Message: ${JSON.stringify(message)}`);
147+
this.messages.push(message);
148+
}
149+
150+
private async executeTool(toolCalls: ChatCompletionMessageToolCall[]): Promise<AgentResponse> {
151+
const toolCall = toolCalls[0];
152+
153+
const tool = this.tools.find(t => t.name === toolCall.function.name);
154+
155+
if (!tool) {
156+
return { output: "Invalid tool_name, please try again", stop: false };
157+
}
158+
159+
this.logger.info(
160+
`tool_call: ${toolCall.function.name}, ${toolCall.function.arguments}`,
161+
);
162+
163+
const functionArgs = JSON.parse(toolCall.function.arguments);
164+
165+
166+
const toolResult = await tool.execute(
167+
Object.fromEntries(
168+
Object.entries(functionArgs).map(([k, v]) => [k, v])
169+
),
170+
this.secrets
171+
);
172+
173+
return {
174+
output: toolResult || '',
175+
stop: false
176+
};
177+
}
178+
}

packages/core/src/llm/index.ts

+6-6
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import OpenAI from 'openai';
2-
import { LLMResult, LLMServiceConfig, Provider } from '../types';
2+
import { AgentResponse, LLMResult, LLMServiceConfig, Provider } from '../types';
33
import { ContentParsingError, InvalidProviderError, LLMModelError, ProviderError } from '../errors';
44
import { ensureError } from '../utils';
55

@@ -76,23 +76,23 @@ export class LLM {
7676
private prepareToolCallResult(message: OpenAI.Chat.ChatCompletionMessage): LLMResult {
7777
return {
7878
toolCalls: message.tool_calls,
79-
content: message.content
79+
content: { output: message.content }
8080
};
8181
}
8282

8383
private prepareContentResult(content: string | null | undefined): LLMResult {
8484
try {
8585
const trimmedContent = content?.trim() ?? "";
86-
const parsed = this.parseJsonContent(trimmedContent);
87-
return { content: parsed.content };
86+
const parsedContent: AgentResponse = this.parseJsonContent(trimmedContent);
87+
return { content: parsedContent };
8888
} catch (error) {
8989
throw new ContentParsingError(
9090
`Failed to prepare content result: ${ensureError(error).message}`
9191
);
9292
}
9393
}
9494

95-
private parseJsonContent(content: string): { content: string } {
95+
private parseJsonContent(content: string): AgentResponse {
9696
try {
9797
return JSON.parse(content);
9898
} catch (error) {
@@ -158,6 +158,6 @@ export class LLM {
158158
stack: normalizedError.stack,
159159
});
160160

161-
return { content: "An error occurred while processing your request. ${errorMessage}" };
161+
return { content: { output: "An error occurred while processing your request. ${errorMessage}" } };
162162
}
163163
}

packages/core/src/tool/index.test.ts

+11-24
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { FunctionInput } from '../types';
55

66
describe('Tool', () => {
77
let validConfig: {
8-
properties?: Record<string, FunctionInput>;
8+
properties: Record<string, FunctionInput>;
99
secrets?: string[];
1010
};
1111

@@ -58,33 +58,33 @@ describe('Tool', () => {
5858
);
5959
});
6060

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

64-
await expect(tool.execute(
64+
expect(() => tool.execute(
6565
{ requiredProp: 'value' },
6666
{ apiKey: 'test-key' }
67-
)).rejects.toThrow(ExecutionError);
67+
)).toThrow(ExecutionError);
6868
});
6969

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

74-
await expect(tool.execute(
74+
expect(() => tool.execute(
7575
{ requiredProp: 'value' },
7676
{}
77-
)).rejects.toThrow(InvalidSecretsError);
77+
)).toThrow(InvalidSecretsError);
7878
});
7979

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

84-
await expect(tool.execute(
84+
expect(() => tool.execute(
8585
{ optionalProp: 'value' },
8686
{ apiKey: 'test-key' }
87-
)).rejects.toThrow(InvalidImplementationError);
87+
)).toThrow(InvalidImplementationError);
8888
});
8989

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

96-
await expect(tool.execute(
96+
expect(() => tool.execute(
9797
{ requiredProp: 'value' },
9898
{ apiKey: 'test-key' }
99-
)).rejects.toThrow(ExecutionError);
99+
)).toThrow(ExecutionError);
100100
});
101101

102-
test('works with no properties configured', async () => {
103-
const implementation = vi.fn().mockReturnValue('success');
104-
const tool = new Tool('testTool', 'Test description', {
105-
secrets: ['apiKey']
106-
}, implementation);
107-
108-
const result = await tool.execute(
109-
{},
110-
{ apiKey: 'test-key' }
111-
);
112-
113-
expect(result).toBe('success');
114-
});
115102

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

124-
const result = await tool.execute(
111+
const result = tool.execute(
125112
{ requiredProp: 'value' }
126113
);
127114

packages/core/src/tool/index.ts

+4-8
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ export class Tool {
88
readonly name: string,
99
readonly description: string,
1010
readonly config: {
11-
properties?: Record<string, FunctionInput>,
11+
properties: Record<string, FunctionInput>,
1212
secrets?: string[],
1313
},
1414
private implementation?: (input: Record<string, unknown>, secrets: Record<string, unknown>) => void
@@ -20,7 +20,7 @@ export class Tool {
2020
this.implementation = implementation;
2121
}
2222

23-
async execute(input: Record<string, unknown>, providedSecrets: Record<string, unknown> = {}): Promise<unknown> {
23+
execute(input: Record<string, unknown>, providedSecrets: Record<string, string> = {}): any {
2424
this.validateSecrets(providedSecrets);
2525
this.validateInput(input);
2626

@@ -44,7 +44,7 @@ export class Tool {
4444
}
4545
}
4646

47-
private validateSecrets(providedSecrets: Record<string, unknown>): void {
47+
private validateSecrets(providedSecrets: Record<string, string>): void {
4848
if (!this.config.secrets) {
4949
return;
5050
}
@@ -55,11 +55,7 @@ export class Tool {
5555
}
5656
}
5757

58-
private validateInput(input: Record<string, unknown>): void {
59-
if (!this.config.properties) {
60-
return;
61-
}
62-
58+
private validateInput(input: Record<string, any>): void {
6359
Object.entries(this.config.properties).forEach(([property, details]) => {
6460
if (details.required && !(property in input)) {
6561
throw new InvalidImplementationError(`Missing required property: ${property}`);

0 commit comments

Comments
 (0)