@betterlu/agentlu 1.0.0
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/README.md +36 -0
- package/dist/index.d.ts +109 -0
- package/dist/index.js +206 -0
- package/package.json +30 -0
package/README.md
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# @betterlu/agentlu
|
|
2
|
+
|
|
3
|
+
Client for the [agentlu](https://github.com/betterlu/agentlu) service API (`/v1`).
|
|
4
|
+
|
|
5
|
+
You can also skip this package and use the OpenAI SDK:
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
import OpenAI from "openai";
|
|
9
|
+
const client = new OpenAI({ baseURL: "http://localhost:8912/v1", apiKey });
|
|
10
|
+
const r = await client.responses.create({ model: "fast", input: "hi" });
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
This client adds get-or-create registration, typed `run({ schema })`, and auto-retry on `409 thread_busy`.
|
|
14
|
+
|
|
15
|
+
## Install
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npm install @betterlu/agentlu
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Usage
|
|
22
|
+
|
|
23
|
+
```ts
|
|
24
|
+
import { AgentLu } from "@betterlu/agentlu";
|
|
25
|
+
|
|
26
|
+
const alu = new AgentLu();
|
|
27
|
+
await alu.register("my-app");
|
|
28
|
+
|
|
29
|
+
const { data, response } = await alu.run({
|
|
30
|
+
agent: "fast",
|
|
31
|
+
input: "hi",
|
|
32
|
+
});
|
|
33
|
+
console.log(response.output_text);
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Env: `AGENTLU_URL` (default `http://localhost:8912/v1`), `AGENTLU_API_KEY`.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @betterlu/agentlu - client for the agentlu service API.
|
|
3
|
+
*
|
|
4
|
+
* You can also skip this package entirely:
|
|
5
|
+
*
|
|
6
|
+
* new OpenAI({ baseURL: "http://localhost:8912/v1", apiKey })
|
|
7
|
+
*
|
|
8
|
+
* This client exists for the ergonomic bits OpenAI's SDK does not know about:
|
|
9
|
+
* get-or-create registration, typed `run({ schema })`, and auto-retry on
|
|
10
|
+
* `409 thread_busy`.
|
|
11
|
+
*/
|
|
12
|
+
export declare const DEFAULT_BASE_URL = "http://localhost:8912/v1";
|
|
13
|
+
export declare class AgentLuError extends Error {
|
|
14
|
+
readonly status: number;
|
|
15
|
+
readonly code: string;
|
|
16
|
+
readonly retryable: boolean;
|
|
17
|
+
constructor(status: number, code: string, message: string, retryable: boolean);
|
|
18
|
+
}
|
|
19
|
+
export interface AgentLuOptions {
|
|
20
|
+
baseUrl?: string;
|
|
21
|
+
apiKey?: string;
|
|
22
|
+
fetch?: typeof fetch;
|
|
23
|
+
}
|
|
24
|
+
export interface RunOptions {
|
|
25
|
+
agent?: string;
|
|
26
|
+
model?: string;
|
|
27
|
+
input: string;
|
|
28
|
+
instructions?: string;
|
|
29
|
+
tools?: Array<string | {
|
|
30
|
+
type: string;
|
|
31
|
+
allowed_tools?: string[];
|
|
32
|
+
}>;
|
|
33
|
+
conversation?: string;
|
|
34
|
+
store?: boolean;
|
|
35
|
+
person?: string | null;
|
|
36
|
+
timeout_ms?: number;
|
|
37
|
+
include?: string[];
|
|
38
|
+
/** JSON Schema. When set, `data` on the result is the validated object. */
|
|
39
|
+
schema?: Record<string, unknown>;
|
|
40
|
+
schemaName?: string;
|
|
41
|
+
}
|
|
42
|
+
export interface ResponseObject {
|
|
43
|
+
id: string;
|
|
44
|
+
status: string;
|
|
45
|
+
output_text: string;
|
|
46
|
+
output_parsed?: unknown;
|
|
47
|
+
conversation?: {
|
|
48
|
+
id: string;
|
|
49
|
+
} | null;
|
|
50
|
+
error?: {
|
|
51
|
+
code: string;
|
|
52
|
+
message: string;
|
|
53
|
+
retryable: boolean;
|
|
54
|
+
} | null;
|
|
55
|
+
agentlu?: {
|
|
56
|
+
session_url?: string | null;
|
|
57
|
+
};
|
|
58
|
+
[k: string]: unknown;
|
|
59
|
+
}
|
|
60
|
+
export declare class AgentLu {
|
|
61
|
+
readonly baseUrl: string;
|
|
62
|
+
apiKey: string | undefined;
|
|
63
|
+
private readonly fetchImpl;
|
|
64
|
+
constructor(opts?: AgentLuOptions);
|
|
65
|
+
/** Get-or-create a client by name. Caches the returned key on this instance. */
|
|
66
|
+
register(name: string, opts?: {
|
|
67
|
+
person?: string | null;
|
|
68
|
+
instructions?: string | null;
|
|
69
|
+
}): Promise<ResponseObject & {
|
|
70
|
+
api_key?: string;
|
|
71
|
+
name: string;
|
|
72
|
+
}>;
|
|
73
|
+
me(): Promise<unknown>;
|
|
74
|
+
/**
|
|
75
|
+
* One-shot helper. When `schema` is given, returns the validated object as
|
|
76
|
+
* `data` and throws `AgentLuError("schema_validation_failed")` if the server
|
|
77
|
+
* could not produce matching JSON.
|
|
78
|
+
*/
|
|
79
|
+
run<T = unknown>(opts: RunOptions): Promise<{
|
|
80
|
+
data?: T;
|
|
81
|
+
response: ResponseObject;
|
|
82
|
+
}>;
|
|
83
|
+
readonly responses: {
|
|
84
|
+
create: (body: Record<string, unknown>) => Promise<ResponseObject>;
|
|
85
|
+
retrieve: (id: string) => Promise<ResponseObject>;
|
|
86
|
+
cancel: (id: string) => Promise<ResponseObject>;
|
|
87
|
+
list: (query?: {
|
|
88
|
+
limit?: number;
|
|
89
|
+
status?: string;
|
|
90
|
+
}) => Promise<{
|
|
91
|
+
object: string;
|
|
92
|
+
data: ResponseObject[];
|
|
93
|
+
}>;
|
|
94
|
+
stream: (body: Record<string, unknown>) => AsyncGenerator<Record<string, unknown>>;
|
|
95
|
+
};
|
|
96
|
+
readonly threads: {
|
|
97
|
+
create: (body?: Record<string, unknown>) => Promise<unknown>;
|
|
98
|
+
retrieve: (id: string) => Promise<unknown>;
|
|
99
|
+
list: () => Promise<unknown>;
|
|
100
|
+
delete: (id: string) => Promise<unknown>;
|
|
101
|
+
messages: (id: string) => Promise<unknown>;
|
|
102
|
+
};
|
|
103
|
+
agents(): Promise<unknown>;
|
|
104
|
+
models(): Promise<unknown>;
|
|
105
|
+
tools(): Promise<unknown>;
|
|
106
|
+
health(): Promise<unknown>;
|
|
107
|
+
private request;
|
|
108
|
+
private raw;
|
|
109
|
+
}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @betterlu/agentlu - client for the agentlu service API.
|
|
3
|
+
*
|
|
4
|
+
* You can also skip this package entirely:
|
|
5
|
+
*
|
|
6
|
+
* new OpenAI({ baseURL: "http://localhost:8912/v1", apiKey })
|
|
7
|
+
*
|
|
8
|
+
* This client exists for the ergonomic bits OpenAI's SDK does not know about:
|
|
9
|
+
* get-or-create registration, typed `run({ schema })`, and auto-retry on
|
|
10
|
+
* `409 thread_busy`.
|
|
11
|
+
*/
|
|
12
|
+
export const DEFAULT_BASE_URL = "http://localhost:8912/v1";
|
|
13
|
+
export class AgentLuError extends Error {
|
|
14
|
+
status;
|
|
15
|
+
code;
|
|
16
|
+
retryable;
|
|
17
|
+
constructor(status, code, message, retryable) {
|
|
18
|
+
super(message);
|
|
19
|
+
this.status = status;
|
|
20
|
+
this.code = code;
|
|
21
|
+
this.retryable = retryable;
|
|
22
|
+
this.name = "AgentLuError";
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
function env(name) {
|
|
26
|
+
return typeof process !== "undefined" ? process.env[name] : undefined;
|
|
27
|
+
}
|
|
28
|
+
export class AgentLu {
|
|
29
|
+
baseUrl;
|
|
30
|
+
apiKey;
|
|
31
|
+
fetchImpl;
|
|
32
|
+
constructor(opts = {}) {
|
|
33
|
+
this.baseUrl = (opts.baseUrl ?? env("AGENTLU_URL") ?? DEFAULT_BASE_URL).replace(/\/$/, "");
|
|
34
|
+
this.apiKey = opts.apiKey ?? env("AGENTLU_API_KEY");
|
|
35
|
+
this.fetchImpl = opts.fetch ?? globalThis.fetch.bind(globalThis);
|
|
36
|
+
}
|
|
37
|
+
/** Get-or-create a client by name. Caches the returned key on this instance. */
|
|
38
|
+
async register(name, opts) {
|
|
39
|
+
const res = await this.request("POST", "/clients", { name, ...opts }, { auth: false });
|
|
40
|
+
if (res.api_key)
|
|
41
|
+
this.apiKey = res.api_key;
|
|
42
|
+
return res;
|
|
43
|
+
}
|
|
44
|
+
async me() {
|
|
45
|
+
return this.request("GET", "/me");
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* One-shot helper. When `schema` is given, returns the validated object as
|
|
49
|
+
* `data` and throws `AgentLuError("schema_validation_failed")` if the server
|
|
50
|
+
* could not produce matching JSON.
|
|
51
|
+
*/
|
|
52
|
+
async run(opts) {
|
|
53
|
+
const body = {
|
|
54
|
+
input: opts.input,
|
|
55
|
+
agent: opts.agent,
|
|
56
|
+
model: opts.model,
|
|
57
|
+
instructions: opts.instructions,
|
|
58
|
+
tools: opts.tools,
|
|
59
|
+
conversation: opts.conversation,
|
|
60
|
+
store: opts.store,
|
|
61
|
+
person: opts.person,
|
|
62
|
+
timeout_ms: opts.timeout_ms,
|
|
63
|
+
include: opts.include,
|
|
64
|
+
};
|
|
65
|
+
if (opts.schema) {
|
|
66
|
+
body.text = {
|
|
67
|
+
format: {
|
|
68
|
+
type: "json_schema",
|
|
69
|
+
name: opts.schemaName ?? "Result",
|
|
70
|
+
schema: opts.schema,
|
|
71
|
+
strict: true,
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
const response = await this.responses.create(body);
|
|
76
|
+
if (response.status === "failed" && response.error) {
|
|
77
|
+
throw new AgentLuError(200, response.error.code, response.error.message, response.error.retryable);
|
|
78
|
+
}
|
|
79
|
+
return { data: response.output_parsed, response };
|
|
80
|
+
}
|
|
81
|
+
responses = {
|
|
82
|
+
create: (body) => this.request("POST", "/responses", body),
|
|
83
|
+
retrieve: (id) => this.request("GET", `/responses/${encodeURIComponent(id)}`),
|
|
84
|
+
cancel: (id) => this.request("POST", `/responses/${encodeURIComponent(id)}/cancel`),
|
|
85
|
+
list: (query) => {
|
|
86
|
+
const q = new URLSearchParams();
|
|
87
|
+
if (query?.limit)
|
|
88
|
+
q.set("limit", String(query.limit));
|
|
89
|
+
if (query?.status)
|
|
90
|
+
q.set("status", query.status);
|
|
91
|
+
const qs = q.toString();
|
|
92
|
+
return this.request("GET", `/responses${qs ? `?${qs}` : ""}`);
|
|
93
|
+
},
|
|
94
|
+
stream: async function* stream(body) {
|
|
95
|
+
const res = await this.raw("POST", "/responses", { ...body, stream: true });
|
|
96
|
+
if (!res.body)
|
|
97
|
+
throw new AgentLuError(0, "internal_error", "empty stream body", true);
|
|
98
|
+
const reader = res.body.getReader();
|
|
99
|
+
const decoder = new TextDecoder();
|
|
100
|
+
let buf = "";
|
|
101
|
+
try {
|
|
102
|
+
for (;;) {
|
|
103
|
+
const { done, value } = await reader.read();
|
|
104
|
+
if (done)
|
|
105
|
+
break;
|
|
106
|
+
buf += decoder.decode(value, { stream: true });
|
|
107
|
+
const lines = buf.split("\n");
|
|
108
|
+
buf = lines.pop() ?? "";
|
|
109
|
+
for (const line of lines) {
|
|
110
|
+
const trimmed = line.trim();
|
|
111
|
+
if (!trimmed.startsWith("data: "))
|
|
112
|
+
continue;
|
|
113
|
+
const payload = trimmed.slice(6);
|
|
114
|
+
if (payload === "[DONE]")
|
|
115
|
+
return;
|
|
116
|
+
try {
|
|
117
|
+
yield JSON.parse(payload);
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
/* ignore malformed chunks */
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
finally {
|
|
126
|
+
reader.releaseLock();
|
|
127
|
+
}
|
|
128
|
+
}.bind(this),
|
|
129
|
+
};
|
|
130
|
+
threads = {
|
|
131
|
+
create: (body = {}) => this.request("POST", "/threads", body),
|
|
132
|
+
retrieve: (id) => this.request("GET", `/threads/${encodeURIComponent(id)}`),
|
|
133
|
+
list: () => this.request("GET", "/threads"),
|
|
134
|
+
delete: (id) => this.request("DELETE", `/threads/${encodeURIComponent(id)}`),
|
|
135
|
+
messages: (id) => this.request("GET", `/threads/${encodeURIComponent(id)}/messages`),
|
|
136
|
+
};
|
|
137
|
+
async agents() {
|
|
138
|
+
return this.request("GET", "/agents", undefined, { auth: false });
|
|
139
|
+
}
|
|
140
|
+
async models() {
|
|
141
|
+
return this.request("GET", "/models", undefined, { auth: false });
|
|
142
|
+
}
|
|
143
|
+
async tools() {
|
|
144
|
+
return this.request("GET", "/tools", undefined, { auth: false });
|
|
145
|
+
}
|
|
146
|
+
async health() {
|
|
147
|
+
return this.request("GET", "/health", undefined, { auth: false });
|
|
148
|
+
}
|
|
149
|
+
async request(method, path, body, opts) {
|
|
150
|
+
// `409 thread_busy` is a correctness bound, not a rate limit. Retrying with
|
|
151
|
+
// a short backoff is almost always the right thing, so the SDK does it.
|
|
152
|
+
let last;
|
|
153
|
+
for (let attempt = 0; attempt < 4; attempt++) {
|
|
154
|
+
try {
|
|
155
|
+
const res = await this.raw(method, path, body, opts);
|
|
156
|
+
if (res.status === 204)
|
|
157
|
+
return undefined;
|
|
158
|
+
const json = (await res.json());
|
|
159
|
+
return json;
|
|
160
|
+
}
|
|
161
|
+
catch (e) {
|
|
162
|
+
if (e instanceof AgentLuError && e.code === "thread_busy" && attempt < 3) {
|
|
163
|
+
last = e;
|
|
164
|
+
await sleep(500 * 2 ** attempt);
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
throw e;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
throw last ?? new AgentLuError(409, "thread_busy", "thread busy", true);
|
|
171
|
+
}
|
|
172
|
+
async raw(method, path, body, opts) {
|
|
173
|
+
const headers = {};
|
|
174
|
+
if (body !== undefined)
|
|
175
|
+
headers["Content-Type"] = "application/json";
|
|
176
|
+
if (opts?.auth !== false && this.apiKey)
|
|
177
|
+
headers.Authorization = `Bearer ${this.apiKey}`;
|
|
178
|
+
const res = await this.fetchImpl(`${this.baseUrl}${path}`, {
|
|
179
|
+
method,
|
|
180
|
+
headers,
|
|
181
|
+
body: body === undefined ? undefined : JSON.stringify(body),
|
|
182
|
+
});
|
|
183
|
+
if (!res.ok) {
|
|
184
|
+
let code = `http_${res.status}`;
|
|
185
|
+
let message = `HTTP ${res.status}`;
|
|
186
|
+
let retryable = res.status >= 500 || res.status === 409;
|
|
187
|
+
try {
|
|
188
|
+
const err = (await res.json());
|
|
189
|
+
if (err.error?.code)
|
|
190
|
+
code = err.error.code;
|
|
191
|
+
if (err.error?.message)
|
|
192
|
+
message = err.error.message;
|
|
193
|
+
if (typeof err.error?.retryable === "boolean")
|
|
194
|
+
retryable = err.error.retryable;
|
|
195
|
+
}
|
|
196
|
+
catch {
|
|
197
|
+
/* keep defaults */
|
|
198
|
+
}
|
|
199
|
+
throw new AgentLuError(res.status, code, message, retryable);
|
|
200
|
+
}
|
|
201
|
+
return res;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
function sleep(ms) {
|
|
205
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
206
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@betterlu/agentlu",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Client for the agentlu service API (/v1). Or just use the openai SDK pointed at http://localhost:8912/v1.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.js"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"files": ["dist"],
|
|
15
|
+
"engines": { "node": ">=18" },
|
|
16
|
+
"scripts": {
|
|
17
|
+
"build": "tsc -p tsconfig.json",
|
|
18
|
+
"prepublishOnly": "npm run build"
|
|
19
|
+
},
|
|
20
|
+
"publishConfig": {
|
|
21
|
+
"access": "public"
|
|
22
|
+
},
|
|
23
|
+
"keywords": ["agentlu", "openai", "responses", "llm", "agent"],
|
|
24
|
+
"repository": {
|
|
25
|
+
"type": "git",
|
|
26
|
+
"url": "git+https://github.com/betterlu/agentlu.git",
|
|
27
|
+
"directory": "packages/sdk"
|
|
28
|
+
},
|
|
29
|
+
"license": "UNLICENSED"
|
|
30
|
+
}
|