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
102 changes: 87 additions & 15 deletions lib/prompt-templates/create-local-circuit-prompt.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,85 @@
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_TSCIRCUIT_DOCS_URL = "https://docs.tscircuit.com/ai.txt"
const GENERATED_TSCIRCUIT_DOCS_TIMEOUT_MS = 1_500

let generatedTscircuitDocsCache: string | undefined
let generatedTscircuitDocsTimeoutMs = GENERATED_TSCIRCUIT_DOCS_TIMEOUT_MS

async function fetchFileContent(
url: string,
init?: RequestInit,
): Promise<string> {
const response = await fetch(url, init)
if (!response.ok) {
throw new Error(
`Failed to fetch file: ${response.status} ${response.statusText}`,
)
}
return await response.text()
}

async function fetchOptionalFileContent(
url: string,
timeoutMs: number,
): Promise<string> {
const abortController = new AbortController()
let timeoutId: ReturnType<typeof setTimeout> | undefined

try {
const response = await fetch(url)
if (!response.ok) {
throw new Error(
`Failed to fetch file: ${response.status} ${response.statusText}`,
)
}
return await response.text()
return await Promise.race([
fetchFileContent(url, { signal: abortController.signal }),
new Promise<string>((resolve) => {
timeoutId = setTimeout(() => {
abortController.abort()
resolve("")
}, timeoutMs)
}),
])
} catch (error) {
console.error("Error fetching file content:", error)
throw error
console.warn(`Optional prompt docs unavailable: ${url}`, error)
return ""
} finally {
if (timeoutId !== undefined) {
clearTimeout(timeoutId)
}
}
}

function cleanMarkdownDoc(doc: string): string {
return doc.trim().replace(/\n\n+/g, "\n\n")
}

async function getGeneratedTscircuitDocs(): Promise<string> {
if (generatedTscircuitDocsCache !== undefined) {
return generatedTscircuitDocsCache
}

const generatedDocs = cleanMarkdownDoc(
await fetchOptionalFileContent(
GENERATED_TSCIRCUIT_DOCS_URL,
generatedTscircuitDocsTimeoutMs,
),
)

generatedTscircuitDocsCache = generatedDocs

return generatedDocs
}

export function resetGeneratedTscircuitDocsCacheForTests() {
generatedTscircuitDocsCache = undefined
generatedTscircuitDocsTimeoutMs = GENERATED_TSCIRCUIT_DOCS_TIMEOUT_MS
}

export function setGeneratedTscircuitDocsTimeoutForTests(timeoutMs: number) {
generatedTscircuitDocsTimeoutMs = timeoutMs
}

export const createLocalCircuitPrompt = async () => {
Expand All @@ -33,22 +96,31 @@ export const createLocalCircuitPrompt = async () => {
"",
)

const propsDoc =
(await fetchFileContent(
"https://raw.githubusercontent.com/tscircuit/props/main/generated/COMPONENT_TYPES.md",
)) || ""
const [propsDoc, generatedTscircuitDocs] = await Promise.all([
fetchFileContent(COMPONENT_TYPES_DOC_URL),
getGeneratedTscircuitDocs(),
])
Comment thread
DevvoZA marked this conversation as resolved.

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

const generatedTscircuitDocsSection = generatedTscircuitDocs
? `## Auto-generated tscircuit documentation

${generatedTscircuitDocs}

`
: ""

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

${generatedTscircuitDocsSection}
## tscircuit API overview

Here's an overview of the tscircuit API:
Expand Down
131 changes: 131 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,131 @@
import { afterEach, beforeEach, describe, expect, test } from "bun:test"
import {
createLocalCircuitPrompt,
resetGeneratedTscircuitDocsCacheForTests,
setGeneratedTscircuitDocsTimeoutForTests,
} from "../../lib/prompt-templates/create-local-circuit-prompt"

const originalFetch = globalThis.fetch

const propsDoc = `# Component Types

<resistor resistance="1k" footprint="0603" />
`

function mockFetch(handler: (url: string) => Response | Promise<Response>) {
globalThis.fetch = (async (input) => {
const url =
typeof input === "string"
? input
: input instanceof URL
? input.toString()
: input.url
return handler(url)
}) as typeof fetch
}

const wait = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms))

beforeEach(() => {
resetGeneratedTscircuitDocsCacheForTests()
})

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

describe("createLocalCircuitPrompt", () => {
test("includes auto-generated docs before the handwritten API overview", async () => {
mockFetch((url) => {
if (url.includes("COMPONENT_TYPES.md")) {
return new Response(propsDoc)
}
if (url.includes("docs.tscircuit.com/ai.txt")) {
return new Response("Generated docs: prefer <chip /> pinLabels.")
}
return new Response("not found", { status: 404 })
})

const prompt = await createLocalCircuitPrompt()

expect(prompt).toContain("## Auto-generated tscircuit documentation")
expect(prompt).toContain("Generated docs: prefer <chip /> pinLabels.")
expect(
prompt.indexOf("## Auto-generated tscircuit documentation"),
).toBeLessThan(prompt.indexOf("## tscircuit API overview"))
})

test("keeps prompt creation working when generated docs are unavailable", async () => {
mockFetch((url) => {
if (url.includes("COMPONENT_TYPES.md")) {
return new Response(propsDoc)
}
if (url.includes("docs.tscircuit.com/ai.txt")) {
return new Response("temporarily unavailable", { status: 503 })
}
return new Response("not found", { status: 404 })
})

const prompt = await createLocalCircuitPrompt()

expect(prompt).not.toContain("## Auto-generated tscircuit documentation")
expect(prompt).toContain("## tscircuit API overview")
expect(prompt).toContain("<resistor resistance")
})

test("caches successful generated docs while still fetching current props docs", async () => {
const calls: string[] = []

mockFetch((url) => {
calls.push(url)
if (url.includes("COMPONENT_TYPES.md")) {
return new Response(propsDoc)
}
if (url.includes("docs.tscircuit.com/ai.txt")) {
return new Response("Generated docs cached once.")
}
return new Response("not found", { status: 404 })
})

await createLocalCircuitPrompt()
await createLocalCircuitPrompt()

expect(
calls.filter((url) => url.includes("docs.tscircuit.com/ai.txt")).length,
).toBe(1)
expect(
calls.filter((url) => url.includes("COMPONENT_TYPES.md")).length,
).toBe(2)
})

test("times out and caches slow optional generated docs", async () => {
const calls: string[] = []
setGeneratedTscircuitDocsTimeoutForTests(1)

mockFetch((url) => {
calls.push(url)
if (url.includes("COMPONENT_TYPES.md")) {
return new Response(propsDoc)
}
if (url.includes("docs.tscircuit.com/ai.txt")) {
return wait(100).then(
() => new Response("Generated docs arrived too late."),
)
}
return new Response("not found", { status: 404 })
})

const prompt = await createLocalCircuitPrompt()
const promptWithCachedTimeout = await createLocalCircuitPrompt()

expect(prompt).not.toContain("Generated docs arrived too late.")
expect(promptWithCachedTimeout).toContain("## tscircuit API overview")
expect(
calls.filter((url) => url.includes("docs.tscircuit.com/ai.txt")).length,
).toBe(1)
expect(
calls.filter((url) => url.includes("COMPONENT_TYPES.md")).length,
).toBe(2)
})
})
77 changes: 41 additions & 36 deletions tests/tscircuitCoder.test.ts
Original file line number Diff line number Diff line change
@@ -1,39 +1,44 @@
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"

test("TscircuitCoder submitPrompt streams and updates vfs", async () => {
const streamedChunks: string[] = []
let vfsUpdated = false
const tscircuitCoder = createTscircuitCoder()
tscircuitCoder.on("streamedChunk", (chunk: string) => {
streamedChunks.push(chunk)
})
tscircuitCoder.on("vfsChanged", () => {
vfsUpdated = true
})

await tscircuitCoder.submitPrompt({
prompt: "create bridge rectifier circuit",
})

await tscircuitCoder.submitPrompt({
prompt: "add a transistor component",
})

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

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

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

expect(streamedChunks.length).toBeGreaterThan(0)
const vfsKeys = Object.keys(tscircuitCoder.vfs)
expect(vfsKeys.length).toBeGreaterThan(0)
expect(vfsUpdated).toBe(true)
})
const testIfOpenAiApiKey = process.env.OPENAI_API_KEY ? test : test.skip

testIfOpenAiApiKey(
"TscircuitCoder submitPrompt streams and updates vfs",
async () => {
const streamedChunks: string[] = []
let vfsUpdated = false
const tscircuitCoder = createTscircuitCoder()
tscircuitCoder.on("streamedChunk", (chunk: string) => {
streamedChunks.push(chunk)
})
tscircuitCoder.on("vfsChanged", () => {
vfsUpdated = true
})

await tscircuitCoder.submitPrompt({
prompt: "create bridge rectifier circuit",
})

await tscircuitCoder.submitPrompt({
prompt: "add a transistor component",
})

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

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

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

expect(streamedChunks.length).toBeGreaterThan(0)
const vfsKeys = Object.keys(tscircuitCoder.vfs)
expect(vfsKeys.length).toBeGreaterThan(0)
expect(vfsUpdated).toBe(true)
},
)
6 changes: 4 additions & 2 deletions tests/utils/generate-random-prompts.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { describe, it, expect } from "bun:test"
import { describe, expect, it } from "bun:test"
import { generateRandomPrompts } from "../../lib/utils/generate-random-prompts"

const itIfOpenAiApiKey = process.env.OPENAI_API_KEY ? it : it.skip

describe("generateRandomPrompts", () => {
it("should return an array of prompts", async () => {
itIfOpenAiApiKey("should return an array of prompts", async () => {
const prompts = await generateRandomPrompts(3)

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