@astrive-ai/core 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/context.d.ts +25 -0
- package/dist/context.d.ts.map +1 -0
- package/dist/context.js +41 -0
- package/dist/context.js.map +1 -0
- package/dist/executor.d.ts +33 -0
- package/dist/executor.d.ts.map +1 -0
- package/dist/executor.js +99 -0
- package/dist/executor.js.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +9 -0
- package/dist/index.js.map +1 -0
- package/dist/middleware-pipeline.d.ts +7 -0
- package/dist/middleware-pipeline.d.ts.map +1 -0
- package/dist/middleware-pipeline.js +28 -0
- package/dist/middleware-pipeline.js.map +1 -0
- package/dist/retry.d.ts +15 -0
- package/dist/retry.d.ts.map +1 -0
- package/dist/retry.js +49 -0
- package/dist/retry.js.map +1 -0
- package/dist/structured-output.d.ts +13 -0
- package/dist/structured-output.d.ts.map +1 -0
- package/dist/structured-output.js +63 -0
- package/dist/structured-output.js.map +1 -0
- package/dist/tool-registry.d.ts +15 -0
- package/dist/tool-registry.d.ts.map +1 -0
- package/dist/tool-registry.js +46 -0
- package/dist/tool-registry.js.map +1 -0
- package/dist/utils.d.ts +16 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +70 -0
- package/dist/utils.js.map +1 -0
- package/dist/validator.d.ts +11 -0
- package/dist/validator.d.ts.map +1 -0
- package/dist/validator.js +85 -0
- package/dist/validator.js.map +1 -0
- package/package.json +26 -0
- package/src/context.ts +64 -0
- package/src/executor.ts +146 -0
- package/src/index.ts +8 -0
- package/src/middleware-pipeline.ts +33 -0
- package/src/retry.ts +62 -0
- package/src/structured-output.ts +80 -0
- package/src/tool-registry.ts +56 -0
- package/src/utils.ts +79 -0
- package/src/validator.ts +92 -0
- package/tsconfig.json +8 -0
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { ChatRequest, JsonSchema } from '@astrive-ai/types';
|
|
2
|
+
export interface ValidationResult {
|
|
3
|
+
valid: boolean;
|
|
4
|
+
errors: string[];
|
|
5
|
+
}
|
|
6
|
+
export declare class RequestValidator {
|
|
7
|
+
validateChatRequest(request: ChatRequest): ValidationResult;
|
|
8
|
+
validateToolArgs(schema: JsonSchema, args: Record<string, unknown>): ValidationResult;
|
|
9
|
+
private validateValue;
|
|
10
|
+
}
|
|
11
|
+
//# sourceMappingURL=validator.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validator.d.ts","sourceRoot":"","sources":["../src/validator.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAE5D,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,OAAO,CAAC;IACf,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB;AAED,qBAAa,gBAAgB;IACpB,mBAAmB,CAAC,OAAO,EAAE,WAAW,GAAG,gBAAgB;IAa3D,gBAAgB,CAAC,MAAM,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,gBAAgB;IAQ5F,OAAO,CAAC,aAAa;CA8DtB"}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
export class RequestValidator {
|
|
2
|
+
validateChatRequest(request) {
|
|
3
|
+
const errors = [];
|
|
4
|
+
if (!request) {
|
|
5
|
+
return { valid: false, errors: ['Request is missing'] };
|
|
6
|
+
}
|
|
7
|
+
if (!Array.isArray(request.messages)) {
|
|
8
|
+
errors.push('request.messages must be an array');
|
|
9
|
+
}
|
|
10
|
+
else if (request.messages.length === 0) {
|
|
11
|
+
errors.push('request.messages cannot be empty');
|
|
12
|
+
}
|
|
13
|
+
return { valid: errors.length === 0, errors };
|
|
14
|
+
}
|
|
15
|
+
validateToolArgs(schema, args) {
|
|
16
|
+
const errors = [];
|
|
17
|
+
this.validateValue(args, schema, '', errors);
|
|
18
|
+
return { valid: errors.length === 0, errors };
|
|
19
|
+
}
|
|
20
|
+
validateValue(value, schema, path, errors) {
|
|
21
|
+
if (value === undefined || value === null) {
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
if (schema.type === 'object') {
|
|
25
|
+
if (typeof value !== 'object' || Array.isArray(value)) {
|
|
26
|
+
errors.push(`'${path}' must be an object`);
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
if (schema.required) {
|
|
30
|
+
for (const req of schema.required) {
|
|
31
|
+
if (value[req] === undefined) {
|
|
32
|
+
errors.push(`'${path ? path + '.' : ''}${req}' is required`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
if (schema.properties) {
|
|
37
|
+
for (const [key, propSchema] of Object.entries(schema.properties)) {
|
|
38
|
+
if (value[key] !== undefined) {
|
|
39
|
+
this.validateValue(value[key], propSchema, path ? `${path}.${key}` : key, errors);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
else if (schema.type === 'array') {
|
|
45
|
+
if (!Array.isArray(value)) {
|
|
46
|
+
errors.push(`'${path}' must be an array`);
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
if (schema.items) {
|
|
50
|
+
for (let i = 0; i < value.length; i++) {
|
|
51
|
+
this.validateValue(value[i], schema.items, `${path}[${i}]`, errors);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
else if (schema.type === 'string') {
|
|
56
|
+
if (typeof value !== 'string') {
|
|
57
|
+
errors.push(`'${path}' must be a string`);
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
if (schema.enum && !schema.enum.includes(value)) {
|
|
61
|
+
errors.push(`'${path}' must be one of: ${schema.enum.join(', ')}`);
|
|
62
|
+
}
|
|
63
|
+
if (schema.pattern) {
|
|
64
|
+
const regex = new RegExp(schema.pattern);
|
|
65
|
+
if (!regex.test(value)) {
|
|
66
|
+
errors.push(`'${path}' must match pattern ${schema.pattern}`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
else if (schema.type === 'number' || schema.type === 'integer') {
|
|
71
|
+
if (typeof value !== 'number') {
|
|
72
|
+
errors.push(`'${path}' must be a number`);
|
|
73
|
+
}
|
|
74
|
+
else if (schema.type === 'integer' && !Number.isInteger(value)) {
|
|
75
|
+
errors.push(`'${path}' must be an integer`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
else if (schema.type === 'boolean') {
|
|
79
|
+
if (typeof value !== 'boolean') {
|
|
80
|
+
errors.push(`'${path}' must be a boolean`);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
//# sourceMappingURL=validator.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validator.js","sourceRoot":"","sources":["../src/validator.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@astrive-ai/core",
|
|
3
|
+
"version": "1.0.1",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"import": "./dist/index.js"
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
"scripts": {
|
|
14
|
+
"build": "tsc",
|
|
15
|
+
"clean": "rm -rf dist"
|
|
16
|
+
},
|
|
17
|
+
"dependencies": {
|
|
18
|
+
"@astrive-ai/types": "*",
|
|
19
|
+
"@astrive-ai/logger": "*",
|
|
20
|
+
"@astrive-ai/events": "*"
|
|
21
|
+
},
|
|
22
|
+
"devDependencies": {
|
|
23
|
+
"typescript": "5.4.5",
|
|
24
|
+
"@types/node": "^20.0.0"
|
|
25
|
+
}
|
|
26
|
+
}
|
package/src/context.ts
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import * as crypto from 'crypto';
|
|
2
|
+
|
|
3
|
+
export interface Message {
|
|
4
|
+
role: 'user' | 'assistant' | 'system' | 'tool';
|
|
5
|
+
content: string;
|
|
6
|
+
name?: string;
|
|
7
|
+
tool_calls?: any[];
|
|
8
|
+
tool_call_id?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface ConversationContext {
|
|
12
|
+
id: string;
|
|
13
|
+
messages: Message[];
|
|
14
|
+
systemPrompt: string;
|
|
15
|
+
metadata: Record<string, unknown>;
|
|
16
|
+
createdAt: number;
|
|
17
|
+
updatedAt: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export class ContextManager {
|
|
21
|
+
private conversations: Map<string, ConversationContext> = new Map();
|
|
22
|
+
|
|
23
|
+
public createConversation(systemPrompt: string = ''): string {
|
|
24
|
+
const id = crypto.randomUUID();
|
|
25
|
+
this.conversations.set(id, {
|
|
26
|
+
id,
|
|
27
|
+
messages: [],
|
|
28
|
+
systemPrompt,
|
|
29
|
+
metadata: {},
|
|
30
|
+
createdAt: Date.now(),
|
|
31
|
+
updatedAt: Date.now()
|
|
32
|
+
});
|
|
33
|
+
return id;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
public addMessage(conversationId: string, message: Message): void {
|
|
37
|
+
const conv = this.conversations.get(conversationId);
|
|
38
|
+
if (!conv) {
|
|
39
|
+
throw new Error(`Conversation not found: ${conversationId}`);
|
|
40
|
+
}
|
|
41
|
+
conv.messages.push(message);
|
|
42
|
+
conv.updatedAt = Date.now();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
public getMessages(conversationId: string): Message[] {
|
|
46
|
+
return this.conversations.get(conversationId)?.messages || [];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
public getConversation(conversationId: string): ConversationContext {
|
|
50
|
+
const conv = this.conversations.get(conversationId);
|
|
51
|
+
if (!conv) {
|
|
52
|
+
throw new Error(`Conversation not found: ${conversationId}`);
|
|
53
|
+
}
|
|
54
|
+
return conv;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
public deleteConversation(conversationId: string): void {
|
|
58
|
+
this.conversations.delete(conversationId);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
public clear(): void {
|
|
62
|
+
this.conversations.clear();
|
|
63
|
+
}
|
|
64
|
+
}
|
package/src/executor.ts
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { ToolRegistry } from './tool-registry.js';
|
|
2
|
+
import { RequestValidator } from './validator.js';
|
|
3
|
+
import { RetryHandler } from './retry.js';
|
|
4
|
+
import { ILogger, IEventEmitter, ToolCall, ToolResult } from '@astrive-ai/types';
|
|
5
|
+
|
|
6
|
+
export interface ExecutorToolResult {
|
|
7
|
+
toolCallId: string;
|
|
8
|
+
result?: ToolResult;
|
|
9
|
+
error?: string;
|
|
10
|
+
status: 'success' | 'error';
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface ExecutorContext {
|
|
14
|
+
[key: string]: any;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface ExecutionStep {
|
|
18
|
+
id: string;
|
|
19
|
+
toolCall: ToolCall;
|
|
20
|
+
dependencies?: string[];
|
|
21
|
+
status?: 'pending' | 'running' | 'completed' | 'failed';
|
|
22
|
+
result?: ExecutorToolResult;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface ExecutionPlan {
|
|
26
|
+
steps: ExecutionStep[];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export class ToolExecutor {
|
|
30
|
+
private registry: ToolRegistry;
|
|
31
|
+
private events: IEventEmitter;
|
|
32
|
+
private logger: ILogger;
|
|
33
|
+
private validator: RequestValidator;
|
|
34
|
+
private retryHandler: RetryHandler;
|
|
35
|
+
|
|
36
|
+
constructor(registry: ToolRegistry, events: IEventEmitter, logger: ILogger) {
|
|
37
|
+
this.registry = registry;
|
|
38
|
+
this.events = events;
|
|
39
|
+
this.logger = logger;
|
|
40
|
+
this.validator = new RequestValidator();
|
|
41
|
+
this.retryHandler = new RetryHandler();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
public async execute(toolCall: ToolCall, context: ExecutorContext = {}): Promise<ExecutorToolResult> {
|
|
45
|
+
await this.events.emit('tool:start', { toolCall });
|
|
46
|
+
this.logger.debug(`Starting tool: ${toolCall.name}`);
|
|
47
|
+
|
|
48
|
+
const tool = this.registry.get(toolCall.name);
|
|
49
|
+
if (!tool) {
|
|
50
|
+
const errorMsg = `Tool not found: ${toolCall.name}`;
|
|
51
|
+
this.logger.error(errorMsg);
|
|
52
|
+
await this.events.emit('tool:end', { toolCall, status: 'error', error: errorMsg });
|
|
53
|
+
return { toolCallId: toolCall.id, error: errorMsg, status: 'error' };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const validation = tool.validate(toolCall.arguments as Record<string, unknown>);
|
|
57
|
+
if (!validation.valid) {
|
|
58
|
+
const errorMsg = `Validation failed for tool ${toolCall.name}: ${validation.errors?.map(e => e.message).join(', ')}`;
|
|
59
|
+
this.logger.error(errorMsg);
|
|
60
|
+
await this.events.emit('tool:end', { toolCall, status: 'error', error: errorMsg });
|
|
61
|
+
return { toolCallId: toolCall.id, error: errorMsg, status: 'error' };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const controller = new AbortController();
|
|
65
|
+
const timeout = setTimeout(() => controller.abort(), 30000);
|
|
66
|
+
|
|
67
|
+
try {
|
|
68
|
+
const executeFn = async () => {
|
|
69
|
+
if (controller.signal.aborted) throw new Error('Timeout');
|
|
70
|
+
return await tool.execute(toolCall.arguments as Record<string, unknown>, { ...context, signal: controller.signal } as any);
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const result = await this.retryHandler.execute(executeFn, {
|
|
74
|
+
maxRetries: 3,
|
|
75
|
+
baseDelay: 1000,
|
|
76
|
+
maxDelay: 10000,
|
|
77
|
+
backoffMultiplier: 2
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
clearTimeout(timeout);
|
|
81
|
+
await this.events.emit('tool:end', { toolCall, status: 'success', result });
|
|
82
|
+
return { toolCallId: toolCall.id, result, status: 'success' };
|
|
83
|
+
} catch (error: any) {
|
|
84
|
+
clearTimeout(timeout);
|
|
85
|
+
this.logger.error(`Error executing tool ${toolCall.name}:`, error);
|
|
86
|
+
await this.events.emit('tool:end', { toolCall, status: 'error', error: error.message });
|
|
87
|
+
return { toolCallId: toolCall.id, error: error.message, status: 'error' };
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
public async executeParallel(toolCalls: ToolCall[], context: ExecutorContext = {}): Promise<ExecutorToolResult[]> {
|
|
92
|
+
const promises = toolCalls.map(tc => this.execute(tc, context));
|
|
93
|
+
return await Promise.all(promises);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
public async executePlan(plan: ExecutionPlan, context: ExecutorContext = {}): Promise<ExecutorToolResult[]> {
|
|
97
|
+
const results: ExecutorToolResult[] = [];
|
|
98
|
+
|
|
99
|
+
for (const step of plan.steps) {
|
|
100
|
+
step.status = 'pending';
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const executeReadySteps = async (): Promise<void> => {
|
|
104
|
+
const readySteps = plan.steps.filter(step =>
|
|
105
|
+
step.status === 'pending' &&
|
|
106
|
+
(!step.dependencies || step.dependencies.every(depId =>
|
|
107
|
+
plan.steps.find(s => s.id === depId)?.status === 'completed'
|
|
108
|
+
))
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
if (readySteps.length === 0) {
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
for (const step of readySteps) {
|
|
116
|
+
step.status = 'running';
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const promises = readySteps.map(async step => {
|
|
120
|
+
const result = await this.execute(step.toolCall, context);
|
|
121
|
+
step.result = result;
|
|
122
|
+
step.status = result.status === 'success' ? 'completed' : 'failed';
|
|
123
|
+
results.push(result);
|
|
124
|
+
if (step.status === 'completed') {
|
|
125
|
+
await executeReadySteps();
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
await Promise.all(promises);
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
await executeReadySteps();
|
|
133
|
+
|
|
134
|
+
const failedSteps = plan.steps.filter(s => s.status === 'failed');
|
|
135
|
+
if (failedSteps.length > 0) {
|
|
136
|
+
this.logger.warn(`Plan execution completed with ${failedSteps.length} failed steps.`);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const pendingSteps = plan.steps.filter(s => s.status === 'pending');
|
|
140
|
+
if (pendingSteps.length > 0) {
|
|
141
|
+
this.logger.error(`Plan execution deadlocked, ${pendingSteps.length} steps remaining pending.`);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return results;
|
|
145
|
+
}
|
|
146
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export * from './context.js';
|
|
2
|
+
export * from './validator.js';
|
|
3
|
+
export * from './middleware-pipeline.js';
|
|
4
|
+
export * from './tool-registry.js';
|
|
5
|
+
export * from './executor.js';
|
|
6
|
+
export * from './retry.js';
|
|
7
|
+
export * from './utils.js';
|
|
8
|
+
export * from './structured-output.js';
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { IMiddleware, MiddlewareContext } from '@astrive-ai/types';
|
|
2
|
+
|
|
3
|
+
export class MiddlewarePipeline {
|
|
4
|
+
private middlewares: IMiddleware[] = [];
|
|
5
|
+
|
|
6
|
+
public use(middleware: IMiddleware): void {
|
|
7
|
+
this.middlewares.push(middleware);
|
|
8
|
+
this.middlewares.sort((a, b) => a.order - b.order);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
public async execute(context: MiddlewareContext): Promise<void> {
|
|
12
|
+
let index = -1;
|
|
13
|
+
|
|
14
|
+
const dispatch = async (i: number): Promise<void> => {
|
|
15
|
+
if (i <= index) {
|
|
16
|
+
throw new Error('next() called multiple times');
|
|
17
|
+
}
|
|
18
|
+
index = i;
|
|
19
|
+
|
|
20
|
+
if (i < this.middlewares.length) {
|
|
21
|
+
const middleware = this.middlewares[i];
|
|
22
|
+
try {
|
|
23
|
+
await middleware.execute(context, () => dispatch(i + 1));
|
|
24
|
+
} catch (error) {
|
|
25
|
+
context.metadata.error = error;
|
|
26
|
+
throw error;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
await dispatch(0);
|
|
32
|
+
}
|
|
33
|
+
}
|
package/src/retry.ts
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
export class AstriveError extends Error {
|
|
2
|
+
public retryable: boolean;
|
|
3
|
+
constructor(message: string, retryable: boolean = false) {
|
|
4
|
+
super(message);
|
|
5
|
+
this.name = 'AstriveError';
|
|
6
|
+
this.retryable = retryable;
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface RetryOptions {
|
|
11
|
+
maxRetries: number;
|
|
12
|
+
baseDelay: number;
|
|
13
|
+
maxDelay: number;
|
|
14
|
+
backoffMultiplier: number;
|
|
15
|
+
retryOn?: (error: Error) => boolean;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export class RetryHandler {
|
|
19
|
+
public async execute<T>(fn: () => Promise<T>, options?: RetryOptions): Promise<T> {
|
|
20
|
+
const opts = {
|
|
21
|
+
maxRetries: 3,
|
|
22
|
+
baseDelay: 1000,
|
|
23
|
+
maxDelay: 10000,
|
|
24
|
+
backoffMultiplier: 2,
|
|
25
|
+
...options
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
let attempt = 0;
|
|
29
|
+
while (true) {
|
|
30
|
+
try {
|
|
31
|
+
return await fn();
|
|
32
|
+
} catch (error: any) {
|
|
33
|
+
if (attempt >= opts.maxRetries) {
|
|
34
|
+
throw error;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
let retryable = false;
|
|
38
|
+
if (opts.retryOn) {
|
|
39
|
+
retryable = opts.retryOn(error);
|
|
40
|
+
} else if (error instanceof AstriveError) {
|
|
41
|
+
retryable = error.retryable;
|
|
42
|
+
} else {
|
|
43
|
+
retryable = true;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (!retryable) {
|
|
47
|
+
throw error;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const delay = Math.min(
|
|
51
|
+
opts.baseDelay * Math.pow(opts.backoffMultiplier, attempt),
|
|
52
|
+
opts.maxDelay
|
|
53
|
+
);
|
|
54
|
+
const jitter = Math.random() * delay * 0.2;
|
|
55
|
+
const sleepTime = delay + jitter;
|
|
56
|
+
|
|
57
|
+
await new Promise(resolve => setTimeout(resolve, sleepTime));
|
|
58
|
+
attempt++;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { IProvider, JsonSchema } from '@astrive-ai/types';
|
|
2
|
+
import { RequestValidator } from './validator.js';
|
|
3
|
+
import { AstriveError } from './retry.js';
|
|
4
|
+
|
|
5
|
+
export interface StructuredOutputOptions {
|
|
6
|
+
schema: JsonSchema;
|
|
7
|
+
maxRetries?: number;
|
|
8
|
+
model?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export class StructuredOutputGenerator {
|
|
12
|
+
private provider: IProvider;
|
|
13
|
+
private validator: RequestValidator;
|
|
14
|
+
|
|
15
|
+
constructor(provider: IProvider) {
|
|
16
|
+
this.provider = provider;
|
|
17
|
+
this.validator = new RequestValidator();
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
public async generate<T>(prompt: string, options: StructuredOutputOptions): Promise<T> {
|
|
21
|
+
const maxRetries = options.maxRetries || 3;
|
|
22
|
+
let currentAttempt = 0;
|
|
23
|
+
|
|
24
|
+
// Convert schema to string
|
|
25
|
+
const schemaString = JSON.stringify(options.schema, null, 2);
|
|
26
|
+
|
|
27
|
+
let currentPrompt = `${prompt}\n\nYou must return a valid JSON object that strictly adheres to the following JSON schema:\n${schemaString}\n\nReturn ONLY the JSON object, with no markdown formatting like \`\`\`json.`;
|
|
28
|
+
|
|
29
|
+
while (currentAttempt < maxRetries) {
|
|
30
|
+
try {
|
|
31
|
+
const response = await this.provider.chat({
|
|
32
|
+
messages: [{ role: 'user', content: currentPrompt }],
|
|
33
|
+
model: options.model,
|
|
34
|
+
responseFormat: { type: 'json_object' }
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
const rawOutput = response.content.trim();
|
|
38
|
+
let parsed: unknown;
|
|
39
|
+
|
|
40
|
+
try {
|
|
41
|
+
// Attempt to parse raw output as JSON
|
|
42
|
+
// Strip markdown code blocks if the model ignored instructions
|
|
43
|
+
let cleanOutput = rawOutput;
|
|
44
|
+
if (cleanOutput.startsWith('```json')) {
|
|
45
|
+
cleanOutput = cleanOutput.substring(7);
|
|
46
|
+
}
|
|
47
|
+
if (cleanOutput.startsWith('```')) {
|
|
48
|
+
cleanOutput = cleanOutput.substring(3);
|
|
49
|
+
}
|
|
50
|
+
if (cleanOutput.endsWith('```')) {
|
|
51
|
+
cleanOutput = cleanOutput.substring(0, cleanOutput.length - 3);
|
|
52
|
+
}
|
|
53
|
+
cleanOutput = cleanOutput.trim();
|
|
54
|
+
|
|
55
|
+
parsed = JSON.parse(cleanOutput);
|
|
56
|
+
} catch (e: any) {
|
|
57
|
+
throw new AstriveError(`Failed to parse JSON: ${e.message}\nRaw output: ${rawOutput}`, true);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Validate against schema
|
|
61
|
+
const validation = this.validator.validateToolArgs(options.schema, parsed as Record<string, unknown>);
|
|
62
|
+
if (!validation.valid) {
|
|
63
|
+
throw new AstriveError(`JSON does not match schema. Errors: ${validation.errors.join(', ')}`, true);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return parsed as T;
|
|
67
|
+
} catch (error: any) {
|
|
68
|
+
currentAttempt++;
|
|
69
|
+
if (currentAttempt >= maxRetries || !(error instanceof AstriveError) || !error.retryable) {
|
|
70
|
+
throw error;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Repair output by telling the model about the error
|
|
74
|
+
currentPrompt = `The previous attempt failed with the following error:\n${error.message}\n\nPlease try again and fix the errors. Remember, you must return a valid JSON object adhering to this schema:\n${schemaString}`;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
throw new Error('Failed to generate structured output after maximum retries');
|
|
79
|
+
}
|
|
80
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { ITool, ToolCategory, ToolDefinition, IEventEmitter } from '@astrive-ai/types';
|
|
2
|
+
|
|
3
|
+
export class ToolRegistry {
|
|
4
|
+
private tools: Map<string, ITool> = new Map();
|
|
5
|
+
private events?: IEventEmitter;
|
|
6
|
+
|
|
7
|
+
constructor(events?: IEventEmitter) {
|
|
8
|
+
this.events = events;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
public register(tool: ITool): void {
|
|
12
|
+
if (!tool.name || !tool.execute) {
|
|
13
|
+
throw new Error(`Invalid tool definition for ${tool.name}`);
|
|
14
|
+
}
|
|
15
|
+
this.tools.set(tool.name, tool);
|
|
16
|
+
if (this.events) {
|
|
17
|
+
this.events.emit('tool:registered', { name: tool.name });
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
public unregister(name: string): void {
|
|
22
|
+
if (this.tools.has(name)) {
|
|
23
|
+
this.tools.delete(name);
|
|
24
|
+
if (this.events) {
|
|
25
|
+
this.events.emit('tool:unregistered', { name });
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
public get(name: string): ITool | undefined {
|
|
31
|
+
return this.tools.get(name);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
public getAll(): ITool[] {
|
|
35
|
+
return Array.from(this.tools.values());
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
public getByCategory(category: ToolCategory): ITool[] {
|
|
39
|
+
return this.getAll().filter(tool => tool.category === category);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
public has(name: string): boolean {
|
|
43
|
+
return this.tools.has(name);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
public getDefinitions(): ToolDefinition[] {
|
|
47
|
+
return this.getAll().map(tool => tool.getSchema());
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
public clear(): void {
|
|
51
|
+
this.tools.clear();
|
|
52
|
+
if (this.events) {
|
|
53
|
+
this.events.emit('tool:cleared');
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
package/src/utils.ts
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import * as crypto from 'crypto';
|
|
2
|
+
import { Buffer } from 'buffer';
|
|
3
|
+
|
|
4
|
+
export function generateId(): string {
|
|
5
|
+
return crypto.randomUUID();
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function sleep(ms: number): Promise<void> {
|
|
9
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function createAbortController(timeoutMs: number): { controller: AbortController; cleanup: () => void } {
|
|
13
|
+
const controller = new AbortController();
|
|
14
|
+
const timeoutId = setTimeout(() => {
|
|
15
|
+
controller.abort();
|
|
16
|
+
}, timeoutMs);
|
|
17
|
+
|
|
18
|
+
return {
|
|
19
|
+
controller,
|
|
20
|
+
cleanup: () => clearTimeout(timeoutId)
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function formatBytes(bytes: number): string {
|
|
25
|
+
if (bytes === 0) return '0 Bytes';
|
|
26
|
+
const k = 1024;
|
|
27
|
+
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
|
|
28
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
29
|
+
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function isUrl(str: string): boolean {
|
|
33
|
+
try {
|
|
34
|
+
new URL(str);
|
|
35
|
+
return true;
|
|
36
|
+
} catch {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export async function resolveImageInput(input: string | Buffer): Promise<Buffer> {
|
|
42
|
+
if (Buffer.isBuffer(input)) {
|
|
43
|
+
return input;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (isUrl(input as string)) {
|
|
47
|
+
const response = await fetch(input as string);
|
|
48
|
+
if (!response.ok) {
|
|
49
|
+
throw new Error(`Failed to fetch image from ${input}: ${response.statusText}`);
|
|
50
|
+
}
|
|
51
|
+
const arrayBuffer = await response.arrayBuffer();
|
|
52
|
+
return Buffer.from(arrayBuffer);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (typeof input === 'string' && input.startsWith('data:image')) {
|
|
56
|
+
const match = input.match(/^data:image\/[^;]+;base64,(.+)$/);
|
|
57
|
+
if (match) {
|
|
58
|
+
return Buffer.from(match[1], 'base64');
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
try {
|
|
63
|
+
const buf = Buffer.from(input as string, 'base64');
|
|
64
|
+
if (buf.toString('base64') === input || buf.toString('base64') === (input as string).replace(/=/g, '')) {
|
|
65
|
+
return buf;
|
|
66
|
+
}
|
|
67
|
+
} catch (e) {
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
throw new Error('Unsupported image input format');
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function toBase64(buffer: Buffer): string {
|
|
74
|
+
return buffer.toString('base64');
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function fromBase64(str: string): Buffer {
|
|
78
|
+
return Buffer.from(str, 'base64');
|
|
79
|
+
}
|