-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
c345881
commit 6f5675d
Showing
10 changed files
with
1,065 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
name: Run tests | ||
on: | ||
push: | ||
branches: [main] | ||
pull_request: | ||
workflow_dispatch: | ||
|
||
jobs: | ||
test: | ||
runs-on: ubuntu-latest | ||
steps: | ||
- uses: actions/checkout@v4 | ||
- uses: pnpm/action-setup@v4 | ||
with: | ||
version: 9 | ||
ref: ${{ github.event.pull_request.head.ref }} | ||
repository: ${{ github.event.pull_request.head.repo.full_name }} | ||
- uses: actions/setup-node@v4 | ||
with: | ||
node-version: 20 | ||
cache: 'pnpm' | ||
- name: Install pnpm dependencies | ||
run: pnpm i | ||
|
||
- name: Run tests | ||
run: pnpm test | ||
|
||
- name: Upload coverage reports | ||
uses: codecov/codecov-action@v4 | ||
with: | ||
token: ${{ secrets.CODECOV_TOKEN }} | ||
file: ./coverage/coverage-final.json | ||
fail_ci_if_error: true |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -128,3 +128,5 @@ dist | |
.yarn/build-state.yml | ||
.yarn/install-state.gz | ||
.pnp.* | ||
|
||
.turbo |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
export class AgentError extends Error { | ||
constructor(message: string, public readonly code: string) { | ||
super(message); | ||
this.name = this.constructor.name; | ||
} | ||
} | ||
|
||
export class InvalidImplementationError extends AgentError { | ||
constructor(message: string) { | ||
super(message, 'INVALID_IMPLEMENTATION'); | ||
} | ||
} | ||
|
||
export class InvalidSecretsError extends AgentError { | ||
constructor(message: string) { | ||
super(message, 'INVALID_SECRETS'); | ||
} | ||
} | ||
|
||
export class ExecutionError extends AgentError { | ||
constructor(message: string) { | ||
super(message, 'EXECUTION_ERROR'); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
{ | ||
"name": "@neuron.js/core", | ||
"version": "0.0.0", | ||
"main": "index.js", | ||
"scripts": { | ||
"test": "vitest --no-watch --no-cache" | ||
}, | ||
"devDependencies": { | ||
"vitest": "^2.1.8" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,131 @@ | ||
import { describe, test, expect, vi, beforeEach } from 'vitest'; | ||
import { Tool } from './index'; | ||
import { ExecutionError, InvalidImplementationError, InvalidSecretsError } from '../errors'; | ||
import { FunctionInput } from '../types'; | ||
|
||
describe('Tool', () => { | ||
let validConfig: { | ||
properties?: Record<string, FunctionInput>; | ||
secrets?: string[]; | ||
}; | ||
|
||
beforeEach(() => { | ||
validConfig = { | ||
properties: { | ||
requiredProp: { type: 'string', description: 'This is a required prop', required: true }, | ||
optionalProp: { type: 'string', description: 'This is an optional prop', required: false } | ||
}, | ||
secrets: ['apiKey'] | ||
}; | ||
}); | ||
|
||
describe('constructor', () => { | ||
test('creates a valid tool with all required properties', () => { | ||
const tool = new Tool('testTool', 'Test description', validConfig); | ||
expect(tool.name).toBe('testTool'); | ||
expect(tool.description).toBe('Test description'); | ||
expect(tool.config).toEqual(validConfig); | ||
}); | ||
|
||
test('throws error when missing required properties', () => { | ||
expect(() => { | ||
new Tool('', 'Test description', validConfig); | ||
}).toThrow(InvalidImplementationError); | ||
|
||
expect(() => { | ||
// @ts-expect-error Testing invalid constructor params | ||
new Tool('Test Tool', 'Test description', null); | ||
}).toThrow(InvalidImplementationError); | ||
}); | ||
}); | ||
|
||
|
||
describe('execute', () => { | ||
test('successfully executes a registered function with valid input and secrets', async () => { | ||
const implementation = vi.fn().mockReturnValue('success'); | ||
const tool = new Tool('testTool', 'Test description', validConfig); | ||
tool.registerFunction(implementation); | ||
|
||
const result = await tool.execute( | ||
{ requiredProp: 'value' }, | ||
{ apiKey: 'test-key' } | ||
); | ||
|
||
expect(result).toBe('success'); | ||
expect(implementation).toHaveBeenCalledWith( | ||
{ requiredProp: 'value' }, | ||
{ apiKey: 'test-key' } | ||
); | ||
}); | ||
|
||
test('throws ExecutionError when no implementation is registered', async () => { | ||
const tool = new Tool('testTool', 'Test description', validConfig); | ||
|
||
await expect(tool.execute( | ||
{ requiredProp: 'value' }, | ||
{ apiKey: 'test-key' } | ||
)).rejects.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( | ||
{ requiredProp: 'value' }, | ||
{} | ||
)).rejects.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( | ||
{ optionalProp: 'value' }, | ||
{ apiKey: 'test-key' } | ||
)).rejects.toThrow(InvalidImplementationError); | ||
}); | ||
|
||
test('handles implementation throwing an error', async () => { | ||
const implementation = vi.fn().mockImplementation(() => { | ||
throw new Error('Implementation error'); | ||
}); | ||
const tool = new Tool('testTool', 'Test description', validConfig, implementation); | ||
|
||
await expect(tool.execute( | ||
{ requiredProp: 'value' }, | ||
{ apiKey: 'test-key' } | ||
)).rejects.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'); | ||
const tool = new Tool('testTool', 'Test description', { | ||
properties: { | ||
requiredProp: { type: 'string', description: 'This is a required prop', required: true } | ||
} | ||
}, implementation); | ||
|
||
const result = await tool.execute( | ||
{ requiredProp: 'value' } | ||
); | ||
|
||
expect(result).toBe('success'); | ||
}); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,69 @@ | ||
import { ExecutionError, InvalidImplementationError, InvalidSecretsError } from '../errors' | ||
import { FunctionInput } from "../types"; | ||
|
||
export class Tool { | ||
private static readonly REQUIRED_PROPERTIES = ['name', 'description', 'config'] as const; | ||
|
||
constructor( | ||
readonly name: string, | ||
readonly description: string, | ||
readonly config: { | ||
properties?: Record<string, FunctionInput>, | ||
secrets?: string[], | ||
}, | ||
private implementation?: (input: Record<string, unknown>, secrets: Record<string, unknown>) => void | ||
) { | ||
this.validateConfig(); | ||
} | ||
|
||
registerFunction(implementation: typeof this.implementation): void { | ||
this.implementation = implementation; | ||
} | ||
|
||
async execute(input: Record<string, unknown>, providedSecrets: Record<string, unknown> = {}): Promise<unknown> { | ||
this.validateSecrets(providedSecrets); | ||
this.validateInput(input); | ||
|
||
if (!this.implementation) { | ||
throw new ExecutionError("No implementation registered"); | ||
} | ||
|
||
try { | ||
return this.implementation(input, providedSecrets); | ||
} catch (error) { | ||
throw new ExecutionError(`Execution failed: ${error instanceof Error ? error.message : 'Unknown error'}`); | ||
} | ||
} | ||
|
||
private validateConfig(): void { | ||
const configKeys = [this.name, this.description, this.config]; | ||
const missingKeys = Tool.REQUIRED_PROPERTIES.filter((_, index) => !configKeys[index]); | ||
|
||
if (missingKeys.length) { | ||
throw new InvalidImplementationError(`Missing required properties: ${missingKeys.join(', ')}`); | ||
} | ||
} | ||
|
||
private validateSecrets(providedSecrets: Record<string, unknown>): void { | ||
if (!this.config.secrets) { | ||
return; | ||
} | ||
|
||
const missingSecrets = this.config.secrets.filter(secret => !(secret in providedSecrets)); | ||
if (missingSecrets.length) { | ||
throw new InvalidSecretsError(`Missing required secrets: ${missingSecrets.join(', ')}`); | ||
} | ||
} | ||
|
||
private validateInput(input: Record<string, unknown>): void { | ||
if (!this.config.properties) { | ||
return; | ||
} | ||
|
||
Object.entries(this.config.properties).forEach(([property, details]) => { | ||
if (details.required && !(property in input)) { | ||
throw new InvalidImplementationError(`Missing required property: ${property}`); | ||
} | ||
}); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
export type FunctionInput = { | ||
type: string; | ||
description: string; | ||
required?: boolean; | ||
}; |
Oops, something went wrong.