Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 26 additions & 11 deletions lib/prompt-templates/create-local-circuit-prompt.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,17 @@
import {
fp,
getFootprintNamesByType,
getFootprintSizes,
fp,
} from "@tscircuit/footprinter"

async function fetchFileContent(url: string): Promise<string> {
const COMPONENT_TYPES_DOC_URL =
"https://raw.githubusercontent.com/tscircuit/props/main/generated/COMPONENT_TYPES.md"
const GENERATED_AI_DOCS_URL = "https://docs.tscircuit.com/ai.txt"

async function fetchFileContent(
url: string,
{ optional = false }: { optional?: boolean } = {},
): Promise<string> {
try {
const response = await fetch(url)
if (!response.ok) {
Expand All @@ -14,11 +21,21 @@ async function fetchFileContent(url: string): Promise<string> {
}
return await response.text()
} catch (error) {
if (optional) return ""
console.error("Error fetching file content:", error)
throw error
}
}

function cleanMarkdownDoc(markdown: string) {
return markdown
.split("\n")
.filter((line) => !line.startsWith("#"))
.join("\n")
.replace(/\n\n+/g, "\n\n")
.trim()
}

export const createLocalCircuitPrompt = async () => {
const footprintNamesByType = getFootprintNamesByType()
const footprintSizes = getFootprintSizes()
Expand All @@ -33,22 +50,20 @@ export const createLocalCircuitPrompt = async () => {
"",
)

const propsDoc =
(await fetchFileContent(
"https://raw.githubusercontent.com/tscircuit/props/main/generated/COMPONENT_TYPES.md",
)) || ""
const [propsDoc, generatedAiDocs] = await Promise.all([
fetchFileContent(COMPONENT_TYPES_DOC_URL),
fetchFileContent(GENERATED_AI_DOCS_URL, { optional: true }),
])

const cleanedPropsDoc = propsDoc
.split("\n")
.filter((line) => !line.startsWith("#"))
.join("\n")
.replace(/\n\n+/g, "\n\n")
const cleanedPropsDoc = cleanMarkdownDoc(propsDoc)
const cleanedGeneratedAiDocs = cleanMarkdownDoc(generatedAiDocs)

return `
You are an expert in electronic circuit design and tscircuit, and your job is to create a circuit board in tscircuit with the user-provided description.

YOU MUST ABIDE BY THE RULES IN THE RULES SECTION

${cleanedGeneratedAiDocs ? `## Auto-generated tscircuit docs\n\nThese generated docs reflect the current tscircuit APIs and examples:\n\n${cleanedGeneratedAiDocs}\n\n` : ""}
## tscircuit API overview

Here's an overview of the tscircuit API:
Expand Down
5 changes: 3 additions & 2 deletions lib/tscircuit-coder/run-ai-with-error-correction.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { askAiWithPreviousAttempts } from "../ask-ai/ask-ai-with-previous-attempts"
import { getPrimarySourceCodeFromVfs } from "lib/utils/get-primary-source-code-from-vfs"
import { saveAttemptLog } from "lib/utils/save-attempt"
import type OpenAI from "openai"
import { askAiWithPreviousAttempts } from "../ask-ai/ask-ai-with-previous-attempts"
import { evaluateTscircuitCode } from "../utils/evaluate-tscircuit-code"
import { getPrimarySourceCodeFromVfs } from "lib/utils/get-primary-source-code-from-vfs"

const createAttemptFile = ({
fileName,
Expand Down Expand Up @@ -136,5 +136,6 @@ export const runAiWithErrorCorrection = async ({
promptNumber,
previousAttempts,
vfs,
openaiClient,
})
}
3 changes: 2 additions & 1 deletion lib/tscircuit-coder/tscircuitCoder.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { EventEmitter } from "node:events"
import { createLocalCircuitPrompt } from "lib/prompt-templates/create-local-circuit-prompt"
import type { OpenAI } from "openai"
import { runAiWithErrorCorrection } from "./run-ai-with-error-correction"
import { createLocalCircuitPrompt } from "lib/prompt-templates/create-local-circuit-prompt"

export interface TscircuitCoderEvents {
streamedChunk: string
Expand Down Expand Up @@ -70,6 +70,7 @@ export class TscircuitCoderImpl extends EventEmitter implements TscircuitCoder {
onStream,
onVfsChanged,
vfs: this.vfs,
openaiClient: this.openaiClient,
})
if (result.code) {
const filepath = `prompt-${promptNumber}-attempt-final.tsx`
Expand Down
3 changes: 2 additions & 1 deletion lib/utils/generate-random-prompts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@ import { openai } from "lib/ai/openai"

export const generateRandomPrompts = async (
numberOfPrompts: number,
openaiClient: typeof openai = openai,
): Promise<string[]> => {
const completion = await openai.chat.completions.create({
const completion = await openaiClient.chat.completions.create({
model: "gpt-4o-mini",

max_tokens: 2048,
Expand Down
64 changes: 64 additions & 0 deletions tests/prompt-templates/create-local-circuit-prompt.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { afterEach, describe, expect, it } from "bun:test"
import { createLocalCircuitPrompt } from "../../lib/prompt-templates/create-local-circuit-prompt"

const originalFetch = globalThis.fetch

afterEach(() => {
globalThis.fetch = originalFetch
})

describe("createLocalCircuitPrompt", () => {
it("includes generated AI docs in the system prompt", async () => {
const fetchCalls: string[] = []

globalThis.fetch = (async (input: RequestInfo | URL) => {
const url = String(input)
fetchCalls.push(url)

if (url === "https://docs.tscircuit.com/ai.txt") {
return new Response(
"# AI docs\nUse <jumper /> for intentional removable connections.",
)
}

return new Response(
"# Component Types\n<resistor /> accepts resistance and footprint props.",
)
}) as typeof fetch

const prompt = await createLocalCircuitPrompt()

expect(fetchCalls).toContain("https://docs.tscircuit.com/ai.txt")
expect(fetchCalls).toContain(
"https://raw.githubusercontent.com/tscircuit/props/main/generated/COMPONENT_TYPES.md",
)
expect(prompt).toContain("## Auto-generated tscircuit docs")
expect(prompt).toContain(
"Use <jumper /> for intentional removable connections.",
)
expect(prompt).toContain(
"<resistor /> accepts resistance and footprint props.",
)
})

it("falls back when generated AI docs cannot be fetched", async () => {
globalThis.fetch = (async (input: RequestInfo | URL) => {
const url = String(input)

if (url === "https://docs.tscircuit.com/ai.txt") {
return new Response("not found", {
status: 404,
statusText: "Not Found",
})
}

return new Response("# Component Types\n<capacitor /> is supported.")
}) as typeof fetch

const prompt = await createLocalCircuitPrompt()

expect(prompt).not.toContain("## Auto-generated tscircuit docs")
expect(prompt).toContain("<capacitor /> is supported.")
expect(prompt).toContain("## tscircuit API overview")
})
})
59 changes: 55 additions & 4 deletions tests/tscircuitCoder.test.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,62 @@
import { createTscircuitCoder } from "lib/tscircuit-coder/tscircuitCoder"
import { expect, test } from "bun:test"
import { createTscircuitCoder } from "lib/tscircuit-coder/tscircuitCoder"
import { getPrimarySourceCodeFromVfs } from "lib/utils/get-primary-source-code-from-vfs"

const circuitForPrompt = (prompt: string) => {
const includeTransistor = /transistor|tssop20/i.test(prompt)
const includeChip = /tssop20/i.test(prompt)

return `
export const GeneratedCircuit = () => (
<board width="50mm" height="40mm">
<chip footprint="soic8" name="U1" pinLabels={{
1: "V+",
2: "V-",
3: "Ref",
4: "In+",
5: "In-",
6: "Out",
7: "Gain",
8: "GND",
}} pcbX="10mm" pcbY="10mm" />
<resistor name="R1" resistance="1k" footprint="0402" pcbX="12mm" pcbY="10mm" />
<resistor name="R2" resistance="1k" footprint="0402" pcbX="10mm" pcbY="12mm" />
${
includeTransistor
? '<resistor name="transistor_bias" resistance="10k" footprint="0402" pcbX="8mm" pcbY="12mm" />'
: ""
}
${
includeChip
? '<chip name="U2" footprint="tssop20" pcbX="20mm" pcbY="10mm" />'
: ""
}
<trace from=".U1 > .pin4" to=".R1 > .pin1" />
<trace from=".U1 > .pin5" to=".R2 > .pin1" />
<trace from=".R1 > .pin2" to=".R2 > .pin2" />
</board>
)
`.trim()
}

const createMockOpenAi = () => ({
chat: {
completions: {
create: async function* ({
messages,
}: { messages: { content: string }[] }) {
const prompt = messages[1]?.content ?? ""
const response = `\`\`\`tsx\n${circuitForPrompt(prompt)}\n\`\`\``
yield { choices: [{ delta: { content: response } }] }
},
},
},
})

test("TscircuitCoder submitPrompt streams and updates vfs", async () => {
const streamedChunks: string[] = []
let vfsUpdated = false
const tscircuitCoder = createTscircuitCoder()
const tscircuitCoder = createTscircuitCoder(createMockOpenAi() as any)
tscircuitCoder.on("streamedChunk", (chunk: string) => {
streamedChunks.push(chunk)
})
Expand All @@ -21,14 +72,14 @@ test("TscircuitCoder submitPrompt streams and updates vfs", async () => {
prompt: "add a transistor component",
})

let codeWithTransistor = getPrimarySourceCodeFromVfs(tscircuitCoder.vfs)
const codeWithTransistor = getPrimarySourceCodeFromVfs(tscircuitCoder.vfs)
expect(codeWithTransistor).toInclude("transistor")

await tscircuitCoder.submitPrompt({
prompt: "add a tssop20 chip",
})

let codeWithChip = getPrimarySourceCodeFromVfs(tscircuitCoder.vfs)
const codeWithChip = getPrimarySourceCodeFromVfs(tscircuitCoder.vfs)
expect(codeWithChip).toInclude("tssop20")
expect(codeWithChip).toInclude("transistor")

Expand Down
24 changes: 22 additions & 2 deletions tests/utils/generate-random-prompts.test.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,29 @@
import { describe, it, expect } from "bun:test"
import { describe, expect, it } from "bun:test"
import { generateRandomPrompts } from "../../lib/utils/generate-random-prompts"

describe("generateRandomPrompts", () => {
it("should return an array of prompts", async () => {
const prompts = await generateRandomPrompts(3)
const openaiClient = {
chat: {
completions: {
create: async () => ({
choices: [
{
message: {
content: [
"1. Create a bridge rectifier with smoothing capacitor",
"2. Design a low-pass RC filter",
"3. Build a transistor switch for an LED",
].join("\n"),
},
},
],
}),
},
},
}

const prompts = await generateRandomPrompts(3, openaiClient as any)

expect(Array.isArray(prompts)).toBe(true)
expect(prompts.length).toBe(3)
Expand Down
Loading