This commit is contained in:
Ariel Weinberger
2025-07-21 10:28:25 -05:00
parent bdc0eb7a01
commit 2c3d3f1deb
9 changed files with 313 additions and 267 deletions
+1 -3
View File
@@ -10,6 +10,4 @@ services:
required: true
environment:
SYNAPSE_ENDPOINT: http://host.docker.internal:3001
APPWRITE_ENDPOINT: http://host.docker.internal:80/v1
volumes:
- ./tmp:/app/tmp
APPWRITE_ENDPOINT: http://host.docker.internal:80/v1
+179 -149
View File
@@ -9,7 +9,6 @@ import {
generateId,
hasToolCall,
smoothStream,
streamObject,
streamText,
TextPart,
tool
@@ -20,9 +19,7 @@ import { anthropic } from '@ai-sdk/anthropic';
import { getOrCreateArtifactSandbox, startDevServer } from '@/lib/daytona/daytona-sandbox';
import { OnStepUpdateFn, workspaceStepSchema } from '@/lib/ai/custom-parts/workspace-state';
import { getOrCreateConversation, updateConversationHistory } from './message-history';
import { imagineToMastraToolset } from '@/lib/ai/mastra/tools/imagine-tool';
import { fileTools } from '@/lib/ai/mastra/tools/file-tools';
import { createSystemPrompt } from '@/lib/ai/system-prompt/create-system-prompt';
import { GitRepositoryUtils } from '@/lib/git-utils';
import {
getPagesAndComponents,
@@ -73,10 +70,14 @@ export const handleChatRequest = async (c: Context) => {
if (trigger === 'submit-tool-result') {
// save to file
// return c.body(null, 200);
return streamText({
model: anthropic('claude-3-7-sonnet-20250219'),
messages: convertToModelMessages(messages)
}).toUIMessageStreamResponse();
// return streamText({
// model: anthropic('claude-3-7-sonnet-20250219'),
// messages: convertToModelMessages(messages)
// }).toUIMessageStreamResponse();
// return c.json({
// error: 'Not implemented'
// }, 500);
console.log('submit-tool-result');
}
const { imagineClient, workspacesClient } = await createImagineClient({
@@ -110,8 +111,8 @@ export const handleChatRequest = async (c: Context) => {
writer.write({
type: 'start',
messageId,
})
messageId
});
const updateStep: OnStepUpdateFn = ({
id,
@@ -130,6 +131,7 @@ export const handleChatRequest = async (c: Context) => {
if (steps.length > 0) {
writer.write({
type: 'data-workspace-state',
transient: true,
data: {
state: 'in-progress',
steps,
@@ -172,7 +174,6 @@ export const handleChatRequest = async (c: Context) => {
}
});
/** SYSTEM PROMPT */
const gitRepositoryUtils = new GitRepositoryUtils(
@@ -201,147 +202,178 @@ export const handleChatRequest = async (c: Context) => {
/** */
const thinkingStartTime = Date.now();
const thinkingId = createIdGenerator({ size: 10 })();
let thoughts = '';
// const thinkingStartTime = Date.now();
// const thinkingId = createIdGenerator({ size: 10 })();
// let thoughts = '';
const planResult = streamText({
maxRetries: 3,
model: anthropic('claude-sonnet-4-20250514'),
abortSignal: signal,
tools: {
readFile: fileTools.readFileTool,
listFilesInDirectory: fileTools.listFilesInDirectoryTool,
readMultipleFilesInParallel: fileTools.readMultipleFilesInParallelTool,
reportPlan: tool({
name: 'reportPlan',
description: 'You use this tool to report your final plan for implementation.',
inputSchema: z.object({
plan: z.string().describe('A detailed plan for implementation. Must be a few paragraphs long.'),
}),
execute: async ({ plan }) => {
console.log('reportPlan', plan);
return 'Your plan has been reported.';
}
})
},
stopWhen: hasToolCall('reportPlan'),
activeTools: ["readFile", "listFilesInDirectory", "readMultipleFilesInParallel", "reportPlan"],
messages: [
{
role: 'system',
content: dedent(`
You are an elite software architect.
// console.log('[planResult] start');
Here is the current file layout and dependencies in the project:
${currentCodeContext.content}
// const planResult = streamText({
// maxRetries: 3,
// model: anthropic('claude-sonnet-4-20250514'),
// abortSignal: signal,
// tools: {
// readFile: fileTools.readFileTool,
// listFilesInDirectory: fileTools.listFilesInDirectoryTool,
// readMultipleFilesInParallel: fileTools.readMultipleFilesInParallelTool,
// reportPlan: tool({
// name: 'reportPlan',
// description:
// 'You use this tool to report your final plan for implementation.',
// inputSchema: z.object({
// plan: z
// .string()
// .describe(
// 'A detailed plan for implementation. Must be a few paragraphs long.'
// )
// }),
// execute: async ({ plan }) => {
// console.log('reportPlan', plan);
// return 'Your plan has been reported.';
// }
// })
// },
// stopWhen: hasToolCall('reportPlan'),
// activeTools: [
// 'readFile',
// 'listFilesInDirectory',
// 'readMultipleFilesInParallel',
// 'reportPlan'
// ],
// messages: [
// {
// role: 'system',
// content: dedent(`
// You are an elite software architect.
// Here is the current file layout and dependencies in the project:
// ${currentCodeContext.content}
Please plan the steps to implement the user's request. You do not write any code.
You may read files to understand the contents of files, which will help you make a better plan.
Your plans should be concise and to the point.
// Please plan the steps to implement the user's request. You do not write any code.
// You may read files to understand the contents of files, which will help you make a better plan.
// Your plans should be concise and to the point.
You will produce the following outputs:
- A short and concise plan
- Whether a UI developer is needed
// You will produce the following outputs:
// - A short and concise plan
// - Whether a UI developer is needed
# Outputs
// # Outputs
## Plan
A short and concise plan of what exactly needs to be done to achieve the product manager's request. This should be 3-4 sentences maximum and in first person. E.g. 'I will ... and then ... '
// ## Plan
// A short and concise plan of what exactly needs to be done to achieve the product manager's request. This should be 3-4 sentences maximum and in first person. E.g. 'I will ... and then ... '
## When is a UI developer needed?
This is important because the UI developer will be the one to implement the design.
// ## When is a UI developer needed?
// This is important because the UI developer will be the one to implement the design.
Examples of when a UI developer IS needed:
- Create new screens
- Modify existing screens with explicit design requests (e.g. adding elements or restructuring the layout)
// Examples of when a UI developer IS needed:
// - Create new screens
// - Modify existing screens with explicit design requests (e.g. adding elements or restructuring the layout)
Examples of when a UI developer IS NOT needed:
- Adding a React hook
- Adding an endpoint
- Pure logic changes
- Moving files around, renaming files, etc.
// Examples of when a UI developer IS NOT needed:
// - Adding a React hook
// - Adding an endpoint
// - Pure logic changes
// - Moving files around, renaming files, etc.
In termso f filesystem tool usage:
- You ONLY have access to read-only filesystem tools
`)
},
...restMessages.map((m: any) => ({
role: m.role,
content: m.content
})),
{
role: 'user',
content: `
// In termso f filesystem tool usage:
// - You ONLY have access to read-only filesystem tools
// `)
// },
// ...restMessages.map((m: any) => ({
// role: m.role,
// content: m.content
// })),
// {
// role: 'user',
// content: `
Here is the product manager's request:
<request>
${latestMessageTextPart.text}
</request>
`
}
],
headers: {
'anthropic-beta': [
'token-efficient-tools-2025-02-19',
'fine-grained-tool-streaming-2025-05-14',
'extended-cache-ttl-2025-04-11'
].join(',')
},
experimental_transform: smoothStream({
delayInMs: 30,
chunking: 'word'
}),
onChunk: (event) => {
if (event.chunk.type === 'text') {
thoughts += event.chunk.text;
// Here is the product manager's request:
// <request>
// ${latestMessageTextPart.text}
// </request>
// `
// }
// ],
// headers: {
// 'anthropic-beta': [
// 'token-efficient-tools-2025-02-19',
// 'fine-grained-tool-streaming-2025-05-14',
// 'extended-cache-ttl-2025-04-11'
// ].join(',')
// },
// experimental_transform: smoothStream({
// delayInMs: 30,
// chunking: 'word'
// }),
// onChunk: (event) => {
// if (event.chunk.type === 'text') {
// thoughts += event.chunk.text;
writer.write({
type: 'data-thinking',
id: thinkingId,
data: {
text: thoughts,
durationMs: null,
state: 'streaming'
// writer.write({
// type: 'data-thinking',
// id: thinkingId,
// data: {
// text: thoughts,
// durationMs: null,
// state: 'streaming'
// }
// });
// }
// },
// onFinish: () => {
// console.log('[planResult] onFinish', { thoughts });
// const thinkingEndTime = Date.now();
// const thinkingDuration = thinkingEndTime - thinkingStartTime;
// writer.write({
// type: 'data-thinking',
// id: thinkingId,
// data: {
// text: thoughts,
// durationMs: thinkingDuration,
// state: 'done'
// }
// });
// }
// });
// for await (const chunk of planResult.textStream) {
// }
// const planToolCall = (await planResult.toolCalls).find(
// (call) => call.toolName === 'reportPlan'
// );
// // if (!planToolCall) {
// // writer.write({
// // type: "finish",
// // })
// // return;
// // }
// const plan = planToolCall?.input?.plan ?? '';
// console.log('[plan]', plan);
console.log('[implementResult] start');
/*
${
plan
? dedent(`
Here is the plan from the architect on how to implement it:
<architect-plan>
${plan}
</architect-plan>`)
: ''
}
});
}
},
onFinish: () => {
const thinkingEndTime = Date.now();
const thinkingDuration = thinkingEndTime - thinkingStartTime;
writer.write({
type: 'data-thinking',
id: thinkingId,
data: {
text: thoughts,
durationMs: thinkingDuration,
state: 'done'
}
});
}
});
for await (const chunk of planResult.textStream) { }
const planToolCall = (await planResult.toolCalls).find((call) => call.toolName === "reportPlan");
if (!planToolCall) {
writer.write({
type: "finish",
})
return;
}
const plan = planToolCall.input.plan;
console.log('plan', plan);
*/
const implementResult = streamText({
maxOutputTokens: 52000,
maxRetries: 5,
model: anthropic('claude-sonnet-4-20250514'),
model: anthropic('claude-3-7-sonnet-20250219'),
stopWhen: hasToolCall('reportDone'),
tools: {
readFile: fileTools.readFileTool,
@@ -385,11 +417,6 @@ export const handleChatRequest = async (c: Context) => {
<product-manager-request>
${latestMessageTextPart.text}
</product-manager-request>
Here is the plan from the architect on how to implement it:
<architect-plan>
${plan}
</architect-plan>
`
}
],
@@ -406,7 +433,7 @@ export const handleChatRequest = async (c: Context) => {
}),
onStepFinish: async (event) => {
console.log('[implementResult onStepFinish] onStepFinish');
const changedFiles = runtimeContext.get('changedFiles');
console.log('[implementResult onStepFinish] changedFiles', changedFiles);
@@ -419,20 +446,23 @@ export const handleChatRequest = async (c: Context) => {
return {
path: f.path,
content: content.content
}
};
})
)
);
await updateDependencies(
filesWithContents
)
await updateDependencies(filesWithContents);
},
onFinish: () => {
console.log('[implementResult] onFinish');
}
});
writer.merge(implementResult.toUIMessageStream({
originalMessages: messages,
sendStart: false,
}));
writer.merge(
implementResult.toUIMessageStream({
originalMessages: messages,
sendStart: false
})
);
},
onFinish: async (event) => {
console.log('onFinish', { didError });
@@ -466,6 +496,6 @@ export const handleChatRequest = async (c: Context) => {
});
return createUIMessageStreamResponse({
stream,
stream
});
};
@@ -25,9 +25,9 @@ const readMultipleFilesInParallelTool = tool({
const runtimeContext = getContext<HonoEnv>().var.runtimeContext;
const writer = getWriterFromContext(runtimeContext);
writer.write({
type: 'start-step'
});
// writer.write({
// type: 'start-step'
// });
console.log(`[TOOL - readMultipleFilesInParallel]`, { paths });
@@ -42,9 +42,9 @@ const readMultipleFilesInParallelTool = tool({
})
);
writer.write({
type: 'finish-step'
});
// writer.write({
// type: 'finish-step'
// });
return { files };
}
@@ -74,9 +74,9 @@ const readFileTool = tool({
const writer = getWriterFromContext(runtimeContext);
const toolCallId = createIdGenerator({ size: 10 })();
writer.write({
type: 'start-step'
});
// writer.write({
// type: 'start-step'
// });
if (!skipWritingToolCalls) {
writer.write({
@@ -92,20 +92,20 @@ const readFileTool = tool({
const result = await readFileFn(`./${path}`);
console.log(`[TOOL - readFile] Reading file at path: ${path}`);
if (!skipWritingToolCalls) {
writer.write({
type: 'tool-output-available',
toolCallId,
output: {
path: result.path,
content: result.content
}
});
}
// if (!skipWritingToolCalls) {
// writer.write({
// type: 'tool-output-available',
// toolCallId,
// output: {
// path: result.path,
// content: result.content
// }
// });
// }
writer.write({
type: 'finish-step'
});
// writer.write({
// type: 'finish-step'
// });
return {
path: result.path,
@@ -140,15 +140,15 @@ const writeFilesTool = tool({
const writer = getWriterFromContext(runtimeContext);
const synapse = runtimeContext.get('synapseClient');
writer.write({
type: 'start-step'
});
// writer.write({
// type: 'start-step'
// });
const { successFiles, errorFiles } = await writeFiles(files, synapse);
writer.write({
type: 'finish-step'
});
// writer.write({
// type: 'finish-step'
// });
return {
successFiles,
@@ -169,7 +169,7 @@ const writeFileTool = tool({
success: z.boolean().describe('Whether the file was written successfully'),
error: z
.string()
.optional()
.nullable()
.describe('The error message if the file was not written successfully')
}),
execute: async ({ path, content }) => {
@@ -178,23 +178,23 @@ const writeFileTool = tool({
const writer = getWriterFromContext(runtimeContext);
const synapse = runtimeContext.get('synapseClient');
writer.write({
type: 'start-step'
});
// writer.write({
// type: 'start-step'
// });
const toolCallId = createIdGenerator({ size: 10 })();
// const toolCallId = createIdGenerator({ size: 10 })();
if (!skipWritingToolCalls) {
writer.write({
type: 'tool-input-available',
toolCallId,
toolName: 'writeFile',
input: {
path,
content
}
});
}
// if (!skipWritingToolCalls) {
// writer.write({
// type: 'tool-input-available',
// toolCallId,
// toolName: 'writeFile',
// input: {
// path,
// content
// }
// });
// }
const { errorFiles, successFiles } = await writeFiles([{ path, content }], synapse);
@@ -204,20 +204,21 @@ const writeFileTool = tool({
console.log('changedFiles', newChangedFiles);
if (!skipWritingToolCalls) {
writer.write({
type: 'tool-output-available',
toolCallId,
output: {
success: true
}
});
}
writer.write({
type: 'finish-step'
});
// if (!skipWritingToolCalls) {
// writer.write({
// type: 'tool-output-available',
// toolCallId,
// output: {
// success: true
// }
// });
// }
// writer.write({
// type: 'finish-step'
// });
return {
success: true
success: true,
error: null
};
}
});
@@ -270,9 +271,9 @@ const listFilesInDirectoryTool = tool({
const writer = getWriterFromContext(runtimeContext);
const synapse = runtimeContext.get('synapseClient');
writer.write({
type: 'start-step'
});
// writer.write({
// type: 'start-step'
// });
console.log(
`[TOOL - listFilesInDirectory] Listing files in directory at path: ${path}, recursive: ${recursive}`
@@ -288,9 +289,9 @@ const listFilesInDirectoryTool = tool({
`[TOOL - listFilesInDirectory] Found ${files.length} files in directory at path: ${path}`
);
writer.write({
type: 'finish-step'
});
// writer.write({
// type: 'finish-step'
// });
return {
files
@@ -313,15 +314,15 @@ const deleteFileTool = tool({
const writer = getWriterFromContext(runtimeContext);
const synapse = runtimeContext.get('synapseClient');
writer.write({
type: 'start-step'
});
// writer.write({
// type: 'start-step'
// });
await synapse.deleteFile({ filepath: `./${path}` });
writer.write({
type: 'finish-step'
});
// writer.write({
// type: 'finish-step'
// });
return {
success: true
@@ -351,15 +352,15 @@ const moveFileTool = tool({
const writer = getWriterFromContext(runtimeContext);
const synapse = runtimeContext.get('synapseClient');
writer.write({
type: 'start-step'
});
// writer.write({
// type: 'start-step'
// });
await synapse.updateFilePath({ filepath: `./${path}`, newPath: `./${newPath}` });
writer.write({
type: 'finish-step'
});
// writer.write({
// type: 'finish-step'
// });
return {
success: true
@@ -8,10 +8,6 @@ RUN npm -v
RUN npm install -g bun
RUN bunx giget --version
RUN ls -la
RUN bunx giget@latest gh:appwrite/templates-for-frameworks/base-vite-template .
RUN bun install
EXPOSE 3000
ENTRYPOINT ["sleep", "infinity"]
+1 -1
View File
@@ -3,6 +3,6 @@ import { Daytona } from "@daytonaio/sdk";
export const daytona = new Daytona({
target: "us",
apiKey: "dtn_4ceb903baf0ef4264e457d2401d904b01c63e11a487d737be0a6113ebeb62ff0",
apiKey: process.env.DAYTONA_API_KEY!,
apiUrl: "https://app.daytona.io/api"
});
+55 -8
View File
@@ -25,10 +25,10 @@ const createSandbox = async ({ artifactId }: { artifactId: string }) => {
let sandbox: Sandbox;
try {
sandbox = await daytona.create({
snapshot: 'imgn-base-vite:v2',
snapshot: 'imgn-base-vite:v4',
labels: { artifactId },
autoStopInterval: 10, // 10 minutes
autoDeleteInterval: 60, // 20 minutes
autoDeleteInterval: 60, // 60 minutes
// language: 'typescript',
public: true
});
@@ -109,6 +109,13 @@ export const getOrCreateArtifactSandbox = async ({
console.log('Sandbox started');
}
onStepUpdate({
id: WorkspaceStepId.CREATE_SANDBOX,
status: 'completed',
text: 'Workspace set up'
});
sandbox = foundSandbox;
} else {
console.log('Workspace not found, creating...');
@@ -123,6 +130,52 @@ export const getOrCreateArtifactSandbox = async ({
artifactId
});
onStepUpdate({
id: WorkspaceStepId.CREATE_SANDBOX,
status: 'completed',
text: 'Workspace created'
});
onStepUpdate({
id: WorkspaceStepId.REPOSITORY_SETUP,
status: 'in-progress',
text: 'Setting up repo...'
});
await sandbox.process.executeCommand(
'bunx giget@latest gh:appwrite/templates-for-frameworks/base-vite-template .',
'/home/daytona/workspace',
{},
30
);
onStepUpdate({
id: WorkspaceStepId.REPOSITORY_SETUP,
status: 'completed',
text: 'Repository set up'
});
onStepUpdate({
id: WorkspaceStepId.INSTALL_DEPENDENCIES,
status: 'in-progress',
text: 'Installing dependencies...'
});
await sandbox.process.executeCommand(
'bun install',
'/home/daytona/workspace',
{},
30
);
onStepUpdate({
id: WorkspaceStepId.INSTALL_DEPENDENCIES,
status: 'completed',
text: 'Dependencies installed'
});
// Save sandbox id to db
await prisma.conversation.update({
where: {
@@ -137,12 +190,6 @@ export const getOrCreateArtifactSandbox = async ({
console.log('Created sandbox', sandbox);
}
onStepUpdate({
id: WorkspaceStepId.CREATE_SANDBOX,
status: 'completed',
text: 'Workspace found'
});
return { sandbox };
};
-24
View File
@@ -17,7 +17,6 @@ export type ListFilesInDirParams = {
export class SynapseHTTPClient {
private sandbox: Sandbox;
private endpoint: string;
private artifactBasePath: string;
private artifactId: string;
constructor({
@@ -30,7 +29,6 @@ export class SynapseHTTPClient {
sandbox: Sandbox;
}) {
this.endpoint = endpoint;
this.artifactBasePath = `/usr/local/artifact`;
this.artifactId = artifactId;
this.sandbox = sandbox;
}
@@ -48,28 +46,6 @@ export class SynapseHTTPClient {
};
}
async createFile({
path,
content,
ignoreBasePath = false
}: {
path: string;
content: string;
ignoreBasePath?: boolean;
}) {
console.log('createFile', { path, content });
const response = await this.request({
type: 'fs',
operation: 'createFile',
params: {
filepath: ignoreBasePath ? path : _path.resolve(this.artifactBasePath, path),
content
}
});
return response;
}
async createOrUpdateFile({ filepath, content }: { filepath: string; content: string }) {
return daytonaFs.createOrUpdateFile({
sandbox: this.sandbox,
+1 -3
View File
@@ -34,15 +34,13 @@
const chat = new Chat<ImagineUIMessage>({
maxSteps: 20,
maxSteps: 50,
id: $conversation.data.id,
transport: new DefaultChatTransport({
api: `${VARS.AI_SERVICE_BASE_URL}/api/chat`,
}),
messages: $conversation.data.messages ?? [],
onData: (dataPart) => {
console.log("data", dataPart);
if (dataPart.type === "data-workspace-state") {
const data = dataPart.data as ImagineUIDataParts["workspace-state"];
const { state, workspaceUrl, steps } = data;