promptgun 1.3.0 → 1.4.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 CHANGED
@@ -1,29 +1,34 @@
1
1
  # Promptgun
2
2
 
3
- The simplest, most advanced LLM prompting and agent library.
3
+ The simplest, most advanced LLM prompting and agent client library for OpenAI:
4
+
5
+ - **Type-safe** structured output, even when streaming
6
+ - **Function calling / tools** with inline or reusable definitions
7
+ - **Feedback loops**, response iteration based on response check
8
+ - **Conversations**: trivally do multiple prompts while retaining context
9
+ - **Strict JSON mode**: a prompting superpower for guaranteed schema compliance
10
+ - **Image generation**: easy-to-use, well-documented, type-safe
11
+ - **Live model docs right in your IDE**, hourly-updated from OpenAI website
12
+ - Future: multi-provider support (drop me a message if you need it)
13
+
14
+ For example:
4
15
 
5
16
  ```typescript
6
17
  const restaurants = ai
7
18
  .chat("Recommend 3 restaurants in Paris perfect for today's weather")
8
- .tools(
9
- defineTool({
10
- name: 'get-weather',
11
- parameters: spec => spec.string(),
12
- function: async location => {
13
- const response = await fetch(`https://api.weather.com/v1/current?location=${location}`)
14
- return response.json()
15
- }
16
- })
17
- )
18
- .getArray(spec => spec.object(o => o
19
+ .tool('get-weather', s => s.string(), async loc => {
20
+ const res = await fetch(`https://api.weather.com/v1/current?location=${loc}`)
21
+ return res.json()
22
+ })
23
+ .getArray(s => s.object(o => o
19
24
  .hasString('name')
20
25
  .hasString('cuisine')
21
26
  .hasString('weatherReason', 'Why this restaurant fits today\'s weather')
22
27
  ))
23
28
 
24
- // Process each restaurant as it streams in
25
29
  for await (const restaurant of restaurants) {
26
- console.log(`${restaurant.name} (${restaurant.cuisine}): ${restaurant.weatherReason}`)
30
+ // do stuff with a type-safe restaurant object,
31
+ // streamed as soon as they come in
27
32
  }
28
33
  ```
29
34
 
@@ -65,6 +70,7 @@ for await (const parsedPartialJson of parsedPartialJsonStream) {
65
70
  ```
66
71
 
67
72
  ### Data output - single
73
+ Simply put `await` in front of your prompt to make it a single output rather than a streamed one:
68
74
  ```typescript
69
75
  const restaurants /* type: {name: string, address?: string}[] */ = await ai
70
76
  .chat('Give 5 top restaurants in London')
@@ -134,22 +140,21 @@ await conversation
134
140
  )
135
141
  ```
136
142
 
137
- ## Response Validation with `.check()`
143
+ ## Response Iteration with `.check()`
138
144
 
139
- Validate AI responses and provide feedback for corrections. The AI will automatically retry with your feedback until validation passes or max attempts are reached.
145
+ Iterate on AI responses by providing feedback for corrections. The AI will automatically retry with your feedback until validation passes or max attempts are reached.
140
146
 
141
147
  ### Basic Example
142
148
  ```typescript
143
149
  const password = await ai
144
150
  .chat("Generate a secure password")
145
- .getObject(o => o.hasString('password'))
146
- .check(response => {
147
- const pwd = response.password;
148
- if (pwd.length < 8) return 'Password must be at least 8 characters';
149
- if (!/[A-Z]/.test(pwd)) return 'Password must contain uppercase letter';
150
- if (!/[0-9]/.test(pwd)) return 'Password must contain a number';
151
+ .getObject(o => o.hasString('pwd'))
152
+ .check(({pwd}) => {
153
+ if (pwd.length < 8) return 'Password must be at least 8 characters'
154
+ if (!/[A-Z]/.test(pwd)) return 'Password must contain uppercase letter'
155
+ if (!/[0-9]/.test(pwd)) return 'Password must contain a number'
151
156
  // Return nothing/null/undefined when validation passes
152
- }, 5); // Allow 5 attempts (default: 10)
157
+ }, 5) // Allow 5 attempts (default: 10)
153
158
  ```
154
159
 
155
160
  ### Multiple Checks
@@ -160,17 +165,18 @@ const user = await ai
160
165
  .getObject(o => o
161
166
  .hasString('email')
162
167
  .hasNumber('age')
163
- .hasString('country'))
164
- .check(response => {
165
- if (!response.email.includes('@')) {
166
- return 'Email must be valid';
168
+ .hasString('country')
169
+ )
170
+ .check(({email}) => {
171
+ if (!email.includes('@')) {
172
+ return 'Email must be valid'
167
173
  }
168
174
  }, 3)
169
- .check(response => {
170
- if (response.age < 0 || response.age > 150) {
171
- return 'Age must be between 0 and 150';
175
+ .check(({age}) => {
176
+ if (age < 0 || age > 150) {
177
+ return 'Age must be between 0 and 150'
172
178
  }
173
- }, 5);
179
+ }, 5)
174
180
  ```
175
181
 
176
182
  ### Error Handling
@@ -179,20 +185,20 @@ You can return errors as strings or throw them:
179
185
  const result = await ai
180
186
  .chat("Calculate the answer")
181
187
  .getObject(o => o.hasNumber('result'))
182
- .check(response => {
188
+ .check(({result}) => {
183
189
  // Option 1: Return error string
184
- if (response.result < 0) return 'Result must be positive';
190
+ if (result < 0) return 'Result must be positive'
185
191
 
186
192
  // Option 2: Throw error
187
- if (response.result > 1000) {
188
- throw new Error('Result too large');
193
+ if (result > 1000) {
194
+ throw new Error('Result too large')
189
195
  }
190
196
 
191
197
  // Option 3: Return array of errors
192
- const errors = [];
193
- if (response.result === 0) errors.push('Result cannot be zero');
194
- if (errors.length > 0) return errors;
195
- });
198
+ const errors = []
199
+ if (result === 0) errors.push('Result cannot be zero')
200
+ return errors
201
+ })
196
202
  ```
197
203
 
198
204
  ### Important Notes
@@ -201,6 +207,112 @@ const result = await ai
201
207
  - **Text responses**: `.check()` works with both JSON (`.getObject()`, `.getArray()`) and text responses
202
208
  - **Feedback loop**: Failed validations are sent back to the AI as feedback, allowing it to learn and correct
203
209
 
210
+ ## Tools
211
+
212
+ ### Inline Tool Definitions
213
+ Tools can be added directly using `.tool()`:
214
+ ```typescript
215
+ const result = await ai
216
+ .chat("What's the weather in Paris?")
217
+ .tool('get-weather', s => s.string(), async location => {
218
+ const res = await fetch(`https://api.weather.com/v1/current?location=${location}`)
219
+ return res.json()
220
+ })
221
+ ```
222
+
223
+ ### Reusable Tool Definitions
224
+ For tools used across multiple prompts, define them once with `aiTool()`:
225
+ ```typescript
226
+ const getWeather = aiTool('get-weather', s => s.string(), async location => {
227
+ const res = await fetch(`https://api.weather.com/v1/current?location=${location}`)
228
+ return res.json()
229
+ })
230
+
231
+ // Use in multiple prompts
232
+ const parisWeather = await ai
233
+ .chat("What's the weather in Paris?")
234
+ .tools(getWeather)
235
+
236
+ const londonWeather = await ai
237
+ .chat("What's the weather in London?")
238
+ .tools(getWeather)
239
+ ```
240
+
241
+ You can pass multiple tools using `.tools()`:
242
+ ```typescript
243
+ await ai
244
+ .chat("Plan a trip")
245
+ .tools(getWeather, getFlights, getHotels)
246
+ ```
247
+
248
+ Or add them one by one with `.tool()`:
249
+ ```typescript
250
+ await ai
251
+ .chat("Plan a trip")
252
+ .tool(getWeather)
253
+ .tool(getFlights)
254
+ .tool(getHotels)
255
+ ```
256
+
257
+ ## Strict JSON Mode
258
+
259
+ Enable OpenAI's strict JSON mode for guaranteed type-safe responses using `.strict()`:
260
+ ```typescript
261
+ const user = await ai
262
+ .chat("Get user info")
263
+ .getObject(o => o
264
+ .hasString('name')
265
+ .hasNumber('age')
266
+ )
267
+ .strict()
268
+ ```
269
+
270
+ ### Important: Nullable vs Optional Properties
271
+
272
+ In strict mode, you **cannot use optional properties** (TypeScript's `?`). Instead, use nullable types:
273
+
274
+ ```typescript
275
+ // ❌ Not allowed in strict mode - optional properties
276
+ const user = await ai
277
+ .chat("Get user info")
278
+ .getObject(o => o
279
+ .hasString('name')
280
+ .canHaveString('nickname') // ❌ This is {nickname?: string}
281
+ )
282
+ .strict()
283
+
284
+ // ✅ Allowed in strict mode - nullable properties
285
+ const user = await ai
286
+ .chat("Get user info")
287
+ .getObject(o => o
288
+ .hasString('name')
289
+ .has('nickname', s => s.string().orNull()) // ✅ This is {nickname: string | null}
290
+ )
291
+ .strict()
292
+ ```
293
+
294
+ **The difference:**
295
+ - **Optional** (`canHaveString`): Property may or may not exist → `{nickname?: string}` → Not allowed in strict mode
296
+ - **Nullable** (`string().orNull()`): Property always exists but value can be null → `{nickname: string | null}` → Allowed in strict mode
297
+
298
+ ## Model Selection
299
+
300
+ Switch models using either a string or the `AiModel` enum:
301
+
302
+ ```typescript
303
+ // Using a string
304
+ const result = await ai
305
+ .chat('Hello')
306
+ .model('gpt-5')
307
+
308
+ // Using the enum - get in-place documentation!
309
+ const result = await ai
310
+ .chat('Hello')
311
+ .model(AiModel.GPT_5)
312
+ ```
313
+
314
+ **Why use the enum?** The `AiModel` enum provides in-place documentation for each model, updated hourly from the OpenAI website. This gives you real-time information about capabilities, context windows, and pricing right in your IDE.
315
+
204
316
  ## Other options
205
317
  Using the flex tier
206
318
  ```typescript
package/build/index.d.ts CHANGED
@@ -1125,6 +1125,17 @@ export declare const AiModel: {
1125
1125
 
1126
1126
  export declare type AiModelId = (typeof AiModel)[keyof typeof AiModel];
1127
1127
 
1128
+ export declare function aiTool<T>(name: string, parameters: (spec: EmptyTypeSpec<unknown>) => TypeSpec<T>, fn: ToolFunction<T>): ToolDefinition<T>;
1129
+
1130
+ export declare function aiTool<T>(name: string, description: string, parameters: (spec: EmptyTypeSpec<unknown>) => TypeSpec<T>, fn: ToolFunction<T>): ToolDefinition<T>;
1131
+
1132
+ export declare function aiTool<T>(config: {
1133
+ name: string;
1134
+ description?: string;
1135
+ parameters: (spec: EmptyTypeSpec<unknown>) => TypeSpec<T>;
1136
+ function: ToolFunction<T>;
1137
+ }): ToolDefinition<T>;
1138
+
1128
1139
  export declare type AnyJson = string | null | boolean | number | JsonArray | JsonObject;
1129
1140
 
1130
1141
  export declare type ApiKeyChain = {
@@ -1246,17 +1257,16 @@ export declare class BasicPrompt<PSArgs> implements AsyncGenerator<string>, Prom
1246
1257
  * This error is propagated immediately without retry delays.
1247
1258
  *
1248
1259
  * @example
1249
- * Validate that a password guess meets requirements:
1260
+ * Iterate on password generation until requirements are met:
1250
1261
  * ```ts
1251
1262
  * const result = await ai
1252
1263
  * .chat("Generate a secure password")
1253
- * .getObject(o => o.hasString('password'))
1254
- * .check(response => {
1255
- * const pwd = response.password;
1256
- * if (pwd.length < 8) return 'Password must be at least 8 characters';
1257
- * if (!/[A-Z]/.test(pwd)) return 'Password must contain uppercase letter';
1258
- * if (!/[0-9]/.test(pwd)) return 'Password must contain a number';
1259
- * }, 5);
1264
+ * .getObject(o => o.hasString('pwd'))
1265
+ * .check(({pwd}) => {
1266
+ * if (pwd.length < 8) return 'Password must be at least 8 characters'
1267
+ * if (!/[A-Z]/.test(pwd)) return 'Password must contain uppercase letter'
1268
+ * if (!/[0-9]/.test(pwd)) return 'Password must contain a number'
1269
+ * }, 5)
1260
1270
  * ```
1261
1271
  */
1262
1272
  check(check: CheckCallback<string>, attempts?: number): BasicPrompt<PSArgs>;
@@ -1280,6 +1290,14 @@ export declare class BasicPrompt<PSArgs> implements AsyncGenerator<string>, Prom
1280
1290
  getObject<T_obj extends {}>(properties: (arg: TypeMemberSpec<{}>) => TypeMemberSpec<T_obj>): JsonPrompt<PSArgs, T_obj>;
1281
1291
  getArray<T_arr extends AnyJson>(element: (arg: EmptyTypeSpec<unknown>) => TypeSpec<T_arr>): JsonArrayPrompt<PSArgs, T_arr>;
1282
1292
  getAny(): JsonPrompt<PSArgs, AnyJson>;
1293
+ tool<T>(name: string, parameters: (spec: EmptyTypeSpec<unknown>) => TypeSpec<T>, fn: ToolFunction<T>): BasicPrompt<PSArgs>;
1294
+ tool<T>(name: string, description: string, parameters: (spec: EmptyTypeSpec<unknown>) => TypeSpec<T>, fn: ToolFunction<T>): BasicPrompt<PSArgs>;
1295
+ tool<T>(config: {
1296
+ name: string;
1297
+ description?: string;
1298
+ parameters: (spec: EmptyTypeSpec<unknown>) => TypeSpec<T>;
1299
+ function: ToolFunction<T>;
1300
+ }): BasicPrompt<PSArgs>;
1283
1301
  tools(tools: ToolDefinition[]): this;
1284
1302
  tools(...tools: (ToolDefinition | undefined)[]): this;
1285
1303
  private getResponseAsString;
@@ -1321,12 +1339,15 @@ export declare type DeepPartial<T> = T extends object ? {
1321
1339
  [P in keyof T]?: DeepPartial<T[P]>;
1322
1340
  } : T;
1323
1341
 
1324
- export declare function defineTool<T>(config: {
1342
+ /**
1343
+ * @deprecated Use {@link aiTool} instead
1344
+ */
1345
+ export declare function defineTool<T>(nameOrConfig: string | {
1325
1346
  name: string;
1326
1347
  description?: string;
1327
1348
  parameters: (spec: EmptyTypeSpec<unknown>) => TypeSpec<T>;
1328
1349
  function: ToolFunction<T>;
1329
- }): ToolDefinition<T>;
1350
+ }, parametersOrDescription?: string | ((spec: EmptyTypeSpec<unknown>) => TypeSpec<T>), fnOrParameters?: ToolFunction<T> | ((spec: EmptyTypeSpec<unknown>) => TypeSpec<T>), maybeFn?: ToolFunction<T>): ToolDefinition<T>;
1330
1351
 
1331
1352
  export declare type DeveloperMessage = {
1332
1353
  system?: undefined;
@@ -1613,21 +1634,22 @@ export declare class JsonPrompt<PSArgs, Json extends AnyJson = AnyJson> extends
1613
1634
  * This error is propagated immediately without retry delays.
1614
1635
  *
1615
1636
  * @example
1616
- * Validate JSON response structure:
1637
+ * Iterate on JSON response until valid:
1617
1638
  * ```ts
1618
1639
  * const user = await ai
1619
1640
  * .chat("Get user info for john@example.com")
1620
1641
  * .getObject(o => o
1621
1642
  * .hasString('email')
1622
- * .hasNumber('age'))
1623
- * .check(response => {
1624
- * if (!response.email.includes('@')) {
1625
- * return 'Email must be valid';
1643
+ * .hasNumber('age')
1644
+ * )
1645
+ * .check(({email, age}) => {
1646
+ * if (!email.includes('@')) {
1647
+ * return 'Email must be valid'
1626
1648
  * }
1627
- * if (response.age < 0 || response.age > 150) {
1628
- * return 'Age must be between 0 and 150';
1649
+ * if (age < 0 || age > 150) {
1650
+ * return 'Age must be between 0 and 150'
1629
1651
  * }
1630
- * }, 3);
1652
+ * }, 3)
1631
1653
  * ```
1632
1654
  */
1633
1655
  check(check: CheckCallback<Json>, attempts?: number): JsonPrompt<PSArgs, Json>;
@@ -1639,6 +1661,14 @@ export declare class JsonPrompt<PSArgs, Json extends AnyJson = AnyJson> extends
1639
1661
  protected getResponse(): Promise<Json>;
1640
1662
  private _responseAsStringPromise?;
1641
1663
  protected getResponseAsString(): Promise<string>;
1664
+ tool<T>(name: string, parameters: (spec: EmptyTypeSpec<unknown>) => TypeSpec<T>, fn: ToolFunction<T>): JsonPrompt<PSArgs, Json>;
1665
+ tool<T>(name: string, description: string, parameters: (spec: EmptyTypeSpec<unknown>) => TypeSpec<T>, fn: ToolFunction<T>): JsonPrompt<PSArgs, Json>;
1666
+ tool<T>(config: {
1667
+ name: string;
1668
+ description?: string;
1669
+ parameters: (spec: EmptyTypeSpec<unknown>) => TypeSpec<T>;
1670
+ function: ToolFunction<T>;
1671
+ }): JsonPrompt<PSArgs, Json>;
1642
1672
  tools(tools: ToolDefinition[]): this;
1643
1673
  tools(...tools: (ToolDefinition | undefined)[]): this;
1644
1674
  protected _getResponseAsString(): Promise<string>;