alchemy 0.3.0 → 0.3.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.
Files changed (107) hide show
  1. package/lib/ai/approve.d.ts +99 -0
  2. package/lib/ai/approve.js +76 -0
  3. package/lib/ai/astro-file.js +7 -24
  4. package/lib/ai/client.d.ts +7 -0
  5. package/lib/ai/client.js +45 -0
  6. package/lib/ai/css-file.js +7 -24
  7. package/lib/ai/data.d.ts +26 -21
  8. package/lib/ai/data.js +33 -22
  9. package/lib/ai/document.d.ts +64 -4
  10. package/lib/ai/document.js +102 -55
  11. package/lib/ai/html-file.js +7 -24
  12. package/lib/ai/index.d.ts +2 -0
  13. package/lib/ai/index.js +2 -0
  14. package/lib/ai/json-file.js +11 -32
  15. package/lib/ai/review.d.ts +122 -0
  16. package/lib/ai/review.js +101 -0
  17. package/lib/ai/typescript-file.d.ts +1 -2
  18. package/lib/ai/typescript-file.js +8 -39
  19. package/lib/ai/vue-file.js +7 -24
  20. package/lib/ai/yaml-file.js +12 -48
  21. package/lib/alchemy.js +17 -2
  22. package/lib/apply.js +1 -1
  23. package/lib/cloudflare/generate-asset-manifest.js +1 -1
  24. package/lib/cloudflare/static-site-router.js +16 -7
  25. package/lib/cloudflare/static-site.js +3 -0
  26. package/lib/cloudflare/upload-asset-manifest.js +4 -2
  27. package/lib/fs/copy-file.d.ts +54 -0
  28. package/lib/fs/copy-file.js +63 -0
  29. package/lib/fs/file.d.ts +13 -0
  30. package/lib/fs/file.js +24 -5
  31. package/lib/fs/index.d.ts +6 -0
  32. package/lib/fs/index.js +6 -0
  33. package/lib/fs/static-astro-file.d.ts +34 -0
  34. package/lib/fs/static-astro-file.js +39 -0
  35. package/lib/fs/static-css-file.d.ts +24 -0
  36. package/lib/fs/static-css-file.js +29 -0
  37. package/lib/fs/static-html-file.d.ts +31 -0
  38. package/lib/fs/static-html-file.js +36 -0
  39. package/lib/fs/static-json-file.d.ts +2 -2
  40. package/lib/fs/static-json-file.js +3 -2
  41. package/lib/fs/static-text-file.d.ts +1 -1
  42. package/lib/fs/static-text-file.js +3 -2
  43. package/lib/fs/static-typescript-file.d.ts +2 -2
  44. package/lib/fs/static-typescript-file.js +3 -2
  45. package/lib/fs/static-vue-file.d.ts +28 -0
  46. package/lib/fs/static-vue-file.js +33 -0
  47. package/lib/fs/static-yaml-file.d.ts +11 -8
  48. package/lib/fs/static-yaml-file.js +3 -2
  49. package/lib/internal/getting-started.d.ts +15 -4
  50. package/lib/internal/getting-started.js +34 -41
  51. package/lib/internal/index.d.ts +3 -0
  52. package/lib/internal/index.js +3 -0
  53. package/lib/internal/providers.d.ts +7 -3
  54. package/lib/internal/providers.js +42 -18
  55. package/lib/internal/tutorial.d.ts +104 -0
  56. package/lib/internal/tutorial.js +251 -0
  57. package/lib/web/vitepress/config.d.ts +10 -0
  58. package/lib/web/vitepress/config.js +31 -0
  59. package/lib/web/vitepress/home-page.d.ts +5 -3
  60. package/lib/web/vitepress/home-page.js +48 -35
  61. package/lib/web/vitepress/index.d.ts +1 -0
  62. package/lib/web/vitepress/index.js +1 -0
  63. package/lib/web/vitepress/vitepress.d.ts +5 -4
  64. package/lib/web/vitepress/vitepress.js +17 -34
  65. package/package.json +1 -1
  66. package/src/ai/approve.ts +163 -0
  67. package/src/ai/astro-file.ts +11 -30
  68. package/src/ai/client.ts +59 -0
  69. package/src/ai/css-file.ts +11 -30
  70. package/src/ai/data.ts +50 -25
  71. package/src/ai/document.ts +146 -65
  72. package/src/ai/html-file.ts +11 -30
  73. package/src/ai/index.ts +2 -0
  74. package/src/ai/json-file.ts +13 -37
  75. package/src/ai/review.ts +213 -0
  76. package/src/ai/typescript-file.ts +14 -48
  77. package/src/ai/vue-file.ts +11 -30
  78. package/src/ai/yaml-file.ts +13 -53
  79. package/src/alchemy.ts +18 -7
  80. package/src/apply.ts +8 -8
  81. package/src/cloudflare/generate-asset-manifest.ts +4 -4
  82. package/src/cloudflare/static-site-router.ts +18 -8
  83. package/src/cloudflare/static-site.ts +11 -7
  84. package/src/cloudflare/upload-asset-manifest.ts +6 -4
  85. package/src/cloudflare/worker.ts +44 -43
  86. package/src/fs/copy-file.ts +115 -0
  87. package/src/fs/file.ts +35 -9
  88. package/src/fs/index.ts +6 -0
  89. package/src/fs/static-astro-file.ts +45 -0
  90. package/src/fs/static-css-file.ts +35 -0
  91. package/src/fs/static-html-file.ts +42 -0
  92. package/src/fs/static-json-file.ts +4 -3
  93. package/src/fs/static-text-file.ts +5 -2
  94. package/src/fs/static-typescript-file.ts +4 -3
  95. package/src/fs/static-vue-file.ts +39 -0
  96. package/src/fs/static-yaml-file.ts +13 -9
  97. package/src/internal/getting-started.ts +51 -43
  98. package/src/internal/index.ts +3 -0
  99. package/src/internal/providers.ts +59 -27
  100. package/src/internal/tutorial.ts +392 -0
  101. package/src/web/vitepress/config.ts +48 -0
  102. package/src/web/vitepress/home-page.ts +54 -42
  103. package/src/web/vitepress/index.ts +1 -0
  104. package/src/web/vitepress/vitepress.ts +31 -48
  105. package/lib/internal/project.d.ts +0 -13
  106. package/lib/internal/project.js +0 -101
  107. package/src/internal/project.ts +0 -119
@@ -0,0 +1,163 @@
1
+ import type { CoreMessage } from "ai";
2
+ import { type } from "arktype";
3
+ import { Data } from "./data";
4
+
5
+ /**
6
+ * Properties for approving or denying content
7
+ */
8
+ export interface ApproveProps {
9
+ /**
10
+ * Content to be reviewed
11
+ * Use alchemy template literals to include file context:
12
+ * @example
13
+ * content: await alchemy`
14
+ * Review this code:
15
+ * ${alchemy.file("src/api.ts")}
16
+ * `
17
+ *
18
+ * Required unless messages are provided
19
+ */
20
+ content?: string;
21
+
22
+ /**
23
+ * Prompt for the approval decision
24
+ * This should include specific criteria for approval or denial
25
+ * @example
26
+ * prompt: "Approve this code if it follows security best practices and has proper error handling."
27
+ *
28
+ * Required unless messages are provided
29
+ */
30
+ prompt?: string;
31
+
32
+ /**
33
+ * System prompt for the model
34
+ * This is used to provide instructions to the model about how to make the decision
35
+ */
36
+ system?: string;
37
+
38
+ /**
39
+ * Message history for the conversation
40
+ * If provided, this will be used instead of the prompt and content
41
+ */
42
+ messages?: CoreMessage[];
43
+
44
+ /**
45
+ * Temperature for controlling randomness in generation.
46
+ * Higher values (e.g., 0.8) make output more random,
47
+ * lower values (e.g., 0.2) make it more deterministic.
48
+ * @default 0.2 (lower for more consistent decisions)
49
+ */
50
+ temperature?: number;
51
+ }
52
+
53
+ /**
54
+ * Result of the approval process
55
+ */
56
+ export interface ApprovalResult {
57
+ /**
58
+ * Whether the content was approved
59
+ */
60
+ approved: boolean;
61
+
62
+ /**
63
+ * Explanation for the decision
64
+ */
65
+ explanation: string;
66
+
67
+ /**
68
+ * Suggestions for improvement if denied
69
+ */
70
+ suggestions?: string[];
71
+
72
+ /**
73
+ * Updated message history with the approval response appended
74
+ */
75
+ messages: CoreMessage[];
76
+ }
77
+
78
+ export type Approve = ApprovalResult;
79
+
80
+ /**
81
+ * Default system prompt for approval decisions
82
+ */
83
+ const DEFAULT_APPROVE_SYSTEM_PROMPT = `You are an expert content reviewer tasked with approving or denying content based on specific criteria.
84
+
85
+ Your role is to:
86
+ 1. Carefully evaluate the content against the provided criteria
87
+ 2. Make a clear decision to approve or deny the content
88
+ 3. Provide a detailed explanation for your decision
89
+ 4. If denying, offer specific suggestions for improvement
90
+
91
+ Be objective, thorough, and fair in your assessment.`;
92
+
93
+ /**
94
+ * Approves or denies content based on a prompt or message history
95
+ *
96
+ * @example
97
+ * // Approve or deny a code file
98
+ * const result = await Approve("code-approval", {
99
+ * content: await alchemy`
100
+ * Review this API implementation:
101
+ * ${alchemy.file("src/api.ts")}
102
+ * `,
103
+ * prompt: "Approve this code if it follows security best practices and has proper error handling."
104
+ * });
105
+ *
106
+ * if (result.approved) {
107
+ * console.log("Content approved:", result.explanation);
108
+ * } else {
109
+ * console.log("Content denied:", result.explanation);
110
+ * console.log("Suggestions:", result.suggestions);
111
+ * }
112
+ *
113
+ * @example
114
+ * // Approve or deny using message history
115
+ * const result = await Approve("doc-approval-with-history", {
116
+ * content: "This is the content to review",
117
+ * prompt: "Approve this documentation if it is clear and accurate",
118
+ * messages: [
119
+ * { role: "user", content: "Can you review this documentation?" },
120
+ * { role: "assistant", content: "Yes, I'd be happy to review it." },
121
+ * { role: "user", content: "Please check for clarity and accuracy." }
122
+ * ]
123
+ * });
124
+ */
125
+ export async function Approve(
126
+ id: string,
127
+ props: ApproveProps
128
+ ): Promise<ApprovalResult> {
129
+ // Create messages array if not provided
130
+ const messages =
131
+ props.messages ||
132
+ (props.content && props.prompt
133
+ ? [
134
+ {
135
+ role: "user" as const,
136
+ content: `${props.content}\n\n${props.prompt}`,
137
+ },
138
+ ]
139
+ : []);
140
+
141
+ if (messages.length === 0) {
142
+ throw new Error(
143
+ "Either messages or both content and prompt must be provided"
144
+ );
145
+ }
146
+
147
+ const data = await Data(id, {
148
+ schema: type({
149
+ approved: "boolean",
150
+ explanation: "string",
151
+ suggestions: "string[]?",
152
+ }),
153
+ messages,
154
+ system: props.system || DEFAULT_APPROVE_SYSTEM_PROMPT,
155
+ temperature: props.temperature ?? 0.2,
156
+ });
157
+
158
+ // Return the result with updated messages from Data
159
+ return {
160
+ ...data.object,
161
+ messages: data.messages,
162
+ };
163
+ }
@@ -1,11 +1,9 @@
1
1
  import { generateText } from "ai";
2
- import fs from "node:fs/promises";
3
- import path from "node:path";
4
2
  import prettier from "prettier";
5
3
  import type { Context } from "../context";
4
+ import { StaticAstroFile } from "../fs/static-astro-file";
6
5
  import { Resource } from "../resource";
7
6
  import type { Secret } from "../secret";
8
- import { ignore } from "../util/ignore";
9
7
  import { type ModelConfig, createModel } from "./client";
10
8
 
11
9
  /**
@@ -161,20 +159,10 @@ export const AstroFile = Resource(
161
159
  async function (
162
160
  this: Context<AstroFile>,
163
161
  id: string,
164
- props: AstroFileProps,
162
+ props: AstroFileProps
165
163
  ): Promise<AstroFile> {
166
- // Ensure directory exists
167
- await fs.mkdir(path.dirname(props.path), { recursive: true });
168
-
164
+ // Handle deletion phase
169
165
  if (this.phase === "delete") {
170
- try {
171
- await fs.unlink(props.path);
172
- } catch (error: any) {
173
- // Ignore if file doesn't exist
174
- if (error.code !== "ENOENT") {
175
- throw error;
176
- }
177
- }
178
166
  return this.destroy();
179
167
  }
180
168
 
@@ -213,7 +201,7 @@ export const AstroFile = Resource(
213
201
 
214
202
  if (retryResult.error) {
215
203
  throw new Error(
216
- `Failed to generate valid Astro code: ${retryResult.error}`,
204
+ `Failed to generate valid Astro code: ${retryResult.error}`
217
205
  );
218
206
  }
219
207
 
@@ -235,24 +223,17 @@ export const AstroFile = Resource(
235
223
  console.warn("Failed to format Astro code with Prettier:", error);
236
224
  }
237
225
 
238
- if (this.phase === "update" && props.path !== this.props.path) {
239
- await ignore("ENOENT", () => fs.unlink(this.props.path));
240
- }
241
-
242
- // Write content to file
243
- await fs.writeFile(props.path, code);
244
-
245
- // Get file stats for timestamps
246
- const stats = await fs.stat(props.path);
226
+ // Use StaticAstroFile to create/update the file
227
+ const file = await StaticAstroFile("file", props.path, code);
247
228
 
248
229
  // Return the resource
249
230
  return this({
250
231
  ...props,
251
- content: code,
252
- createdAt: stats.birthtimeMs,
253
- updatedAt: stats.mtimeMs,
232
+ content: file.content,
233
+ createdAt: Date.now(),
234
+ updatedAt: Date.now(),
254
235
  });
255
- },
236
+ }
256
237
  );
257
238
 
258
239
  /**
@@ -263,7 +244,7 @@ export const AstroFile = Resource(
263
244
  * @returns The extracted Astro code or error message
264
245
  */
265
246
  async function extractAstroCode(
266
- text: string,
247
+ text: string
267
248
  ): Promise<{ code: string; error?: string }> {
268
249
  const astroCodeRegex = /```astro\s*([\s\S]*?)```/g;
269
250
  const matches = Array.from(text.matchAll(astroCodeRegex));
package/src/ai/client.ts CHANGED
@@ -56,3 +56,62 @@ export function createModel(config: ClientConfig) {
56
56
  return openai(config.model?.id ?? "gpt-4o");
57
57
  }
58
58
  }
59
+
60
+ /**
61
+ * Maximum time to retry in milliseconds (5 minutes)
62
+ */
63
+ const MAX_RETRY_TIME = 5 * 60 * 1000;
64
+
65
+ /**
66
+ * Initial delay between retries in milliseconds
67
+ */
68
+ const INITIAL_RETRY_DELAY = 1000;
69
+
70
+ /**
71
+ * Maximum number of retries
72
+ */
73
+ const MAX_RETRIES = 10;
74
+
75
+ /**
76
+ * Handles rate limiting with exponential backoff
77
+ * @param fn Function to retry
78
+ * @returns Result of the function
79
+ * @throws Error if max retries or time is exceeded
80
+ */
81
+ export async function withRateLimitRetry<T>(fn: () => Promise<T>): Promise<T> {
82
+ let retryCount = 0;
83
+ let lastError: Error | null = null;
84
+ let startTime = Date.now();
85
+
86
+ while (true) {
87
+ try {
88
+ return await fn();
89
+ } catch (error: any) {
90
+ lastError = error;
91
+
92
+ console.log("retry error", error);
93
+
94
+ // Check if we should retry
95
+ const isRateLimit = error.statusCode === 429;
96
+ const timeElapsed = Date.now() - startTime;
97
+ const shouldRetry =
98
+ isRateLimit && retryCount < MAX_RETRIES && timeElapsed < MAX_RETRY_TIME;
99
+
100
+ if (!shouldRetry) {
101
+ throw error;
102
+ }
103
+
104
+ // Calculate delay with exponential backoff
105
+ const delay = Math.min(
106
+ INITIAL_RETRY_DELAY * Math.pow(2, retryCount),
107
+ MAX_RETRY_TIME - timeElapsed
108
+ );
109
+
110
+ console.log(`Retrying in ${delay}ms`);
111
+
112
+ // Wait before retrying
113
+ await new Promise((resolve) => setTimeout(resolve, delay));
114
+ retryCount++;
115
+ }
116
+ }
117
+ }
@@ -1,10 +1,8 @@
1
1
  import { generateText } from "ai";
2
- import fs from "node:fs/promises";
3
- import path from "node:path";
4
2
  import type { Context } from "../context";
3
+ import { StaticCSSFile } from "../fs/static-css-file";
5
4
  import { Resource } from "../resource";
6
5
  import type { Secret } from "../secret";
7
- import { ignore } from "../util/ignore";
8
6
  import { type ModelConfig, createModel } from "./client";
9
7
 
10
8
  /**
@@ -154,20 +152,10 @@ export const CSSFile = Resource(
154
152
  async function (
155
153
  this: Context<CSSFile>,
156
154
  id: string,
157
- props: CSSFileProps,
155
+ props: CSSFileProps
158
156
  ): Promise<CSSFile> {
159
- // Ensure directory exists
160
- await fs.mkdir(path.dirname(props.path), { recursive: true });
161
-
157
+ // Handle deletion phase
162
158
  if (this.phase === "delete") {
163
- try {
164
- await fs.unlink(props.path);
165
- } catch (error: any) {
166
- // Ignore if file doesn't exist
167
- if (error.code !== "ENOENT") {
168
- throw error;
169
- }
170
- }
171
159
  return this.destroy();
172
160
  }
173
161
 
@@ -206,31 +194,24 @@ export const CSSFile = Resource(
206
194
 
207
195
  if (retryResult.error) {
208
196
  throw new Error(
209
- `Failed to generate valid CSS code: ${retryResult.error}\n${retryText}`,
197
+ `Failed to generate valid CSS code: ${retryResult.error}\n${retryText}`
210
198
  );
211
199
  }
212
200
 
213
201
  code = retryResult.code;
214
202
  }
215
203
 
216
- if (this.phase === "update" && props.path !== this.props.path) {
217
- await ignore("ENOENT", () => fs.unlink(this.props.path));
218
- }
219
-
220
- // Write content to file
221
- await fs.writeFile(props.path, code);
222
-
223
- // Get file stats for timestamps
224
- const stats = await fs.stat(props.path);
204
+ // Use StaticCSSFile to create/update the file
205
+ const file = await StaticCSSFile("file", props.path, code);
225
206
 
226
207
  // Return the resource
227
208
  return this({
228
209
  ...props,
229
- content: code,
230
- createdAt: stats.birthtimeMs,
231
- updatedAt: stats.mtimeMs,
210
+ content: file.content,
211
+ createdAt: Date.now(),
212
+ updatedAt: Date.now(),
232
213
  });
233
- },
214
+ }
234
215
  );
235
216
 
236
217
  /**
@@ -241,7 +222,7 @@ export const CSSFile = Resource(
241
222
  * @returns The extracted CSS code or error message
242
223
  */
243
224
  async function extractCSSCode(
244
- text: string,
225
+ text: string
245
226
  ): Promise<{ code: string; error?: string }> {
246
227
  const cssCodeRegex = /```css\s*([\s\S]*?)```/g;
247
228
  const matches = Array.from(text.matchAll(cssCodeRegex));
package/src/ai/data.ts CHANGED
@@ -1,10 +1,10 @@
1
- import { generateObject } from "ai";
1
+ import { generateObject, type CoreMessage } from "ai";
2
2
  import type { JsonSchema, Type, type } from "arktype";
3
3
  import type { Context } from "../context";
4
4
  import { Resource } from "../resource";
5
5
  import type { Secret } from "../secret";
6
6
  import { ark } from "./ark";
7
- import { type ModelConfig, createModel } from "./client";
7
+ import { createModel, type ModelConfig } from "./client";
8
8
 
9
9
  /**
10
10
  * Properties for creating or updating an AI Object
@@ -24,7 +24,13 @@ export interface DataProps<T extends Type<any, any>> {
24
24
  * ${alchemy.file("src/data.ts")}
25
25
  * `
26
26
  */
27
- prompt: string;
27
+ prompt?: string;
28
+
29
+ /**
30
+ * Message history for the conversation
31
+ * If provided, this will be used instead of the prompt
32
+ */
33
+ messages?: CoreMessage[];
28
34
 
29
35
  /**
30
36
  * System prompt to guide the AI's behavior
@@ -70,6 +76,11 @@ export interface Data<T> extends Resource<"ai::Object"> {
70
76
  */
71
77
  object: T;
72
78
 
79
+ /**
80
+ * Updated message history with the AI's response appended
81
+ */
82
+ messages: CoreMessage[];
83
+
73
84
  /**
74
85
  * Time at which the content was generated
75
86
  */
@@ -127,28 +138,23 @@ export interface Data<T> extends Resource<"ai::Object"> {
127
138
  * });
128
139
  *
129
140
  * @example
130
- * // Using specific model configuration with advanced options
131
- * const analysisSchema = type({
132
- * insights: "string[]",
133
- * recommendations: "string[]",
134
- * risk: "'low'|'medium'|'high'"
141
+ * // Using message history for iterative generation
142
+ * const feedbackSchema = type({
143
+ * rating: "number",
144
+ * positives: "string[]",
145
+ * improvements: "string[]",
146
+ * summary: "string"
135
147
  * });
136
148
  *
137
- * const analysis = await Data("code-analysis", {
138
- * schema: analysisSchema,
139
- * prompt: await alchemy`
140
- * Analyze this code for security issues:
141
- * ${alchemy.file("src/auth/login.ts")}
142
- * `,
143
- * system: "You are a security expert specializing in code analysis",
144
- * model: {
145
- * id: "o3-mini",
146
- * provider: "openai",
147
- * options: {
148
- * reasoningEffort: "high"
149
- * }
150
- * },
151
- * temperature: 0.1
149
+ * const feedback = await Data("product-feedback", {
150
+ * schema: feedbackSchema,
151
+ * messages: [
152
+ * { role: "user", content: "I'd like feedback on my product design" },
153
+ * { role: "assistant", content: "I'd be happy to provide feedback. What's your product?" },
154
+ * { role: "user", content: "It's a new smart home device that..." }
155
+ * ],
156
+ * system: "You are a product design expert providing structured feedback",
157
+ * temperature: 0.3
152
158
  * });
153
159
  */
154
160
  export const Data = Resource("ai::Object", async function <
@@ -160,6 +166,14 @@ export const Data = Resource("ai::Object", async function <
160
166
  return this.destroy();
161
167
  }
162
168
 
169
+ // Validate that either prompt or messages is provided
170
+ if (!props.prompt && !props.messages) {
171
+ throw new Error("Either prompt or messages must be provided");
172
+ }
173
+
174
+ // Create messages array if only prompt is provided
175
+ const messages = props.messages || [{ role: "user", content: props.prompt! }];
176
+
163
177
  // Generate structured output using generateObject
164
178
  const { object } = await generateObject({
165
179
  model: createModel(props),
@@ -170,17 +184,28 @@ export const Data = Resource("ai::Object", async function <
170
184
  system:
171
185
  props.system ||
172
186
  "You are an AI assistant tasked with generating structured content.",
173
- prompt: props.prompt,
187
+ messages,
174
188
  ...(props.temperature === undefined
175
189
  ? {}
176
190
  : // some models error if you provide it (rather than ignoring it)
177
191
  { temperature: props.temperature }),
178
192
  });
179
193
 
180
- // Return the resource with typed content
194
+ // Create updated message history with the structured response
195
+ const responseText = JSON.stringify(object);
196
+ const updatedMessages = [
197
+ ...messages,
198
+ {
199
+ role: "assistant" as const,
200
+ content: responseText,
201
+ },
202
+ ];
203
+
204
+ // Return the resource with typed content and updated messages
181
205
  return this({
182
206
  type: props.schema,
183
207
  object: object,
208
+ messages: updatedMessages,
184
209
  createdAt: Date.now(),
185
210
  });
186
211
  });