From 5145af3d39c8ef37c142df0917c6f9aad3b06d6e Mon Sep 17 00:00:00 2001 From: DevvoZA <101029583+DevvoZA@users.noreply.github.com> Date: Sat, 23 May 2026 22:38:50 -0400 Subject: [PATCH 1/2] Include generated docs in local circuit prompt --- .../create-local-circuit-prompt.ts | 71 +++++++++++--- .../create-local-circuit-prompt.test.ts | 98 +++++++++++++++++++ tests/tscircuitCoder.test.ts | 77 ++++++++------- tests/utils/generate-random-prompts.test.ts | 6 +- 4 files changed, 200 insertions(+), 52 deletions(-) create mode 100644 tests/prompt-templates/create-local-circuit-prompt.test.ts diff --git a/lib/prompt-templates/create-local-circuit-prompt.ts b/lib/prompt-templates/create-local-circuit-prompt.ts index a93f11f..fe5a7b8 100644 --- a/lib/prompt-templates/create-local-circuit-prompt.ts +++ b/lib/prompt-templates/create-local-circuit-prompt.ts @@ -1,22 +1,56 @@ import { + fp, getFootprintNamesByType, getFootprintSizes, - fp, } from "@tscircuit/footprinter" +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" + +let generatedTscircuitDocsCache: string | undefined + async function fetchFileContent(url: string): Promise { + const response = await fetch(url) + if (!response.ok) { + throw new Error( + `Failed to fetch file: ${response.status} ${response.statusText}`, + ) + } + return await response.text() +} + +async function fetchOptionalFileContent(url: string): Promise { 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 fetchFileContent(url) } catch (error) { - console.error("Error fetching file content:", error) - throw error + console.warn(`Optional prompt docs unavailable: ${url}`, error) + return "" + } +} + +function cleanMarkdownDoc(doc: string): string { + return doc.trim().replace(/\n\n+/g, "\n\n") +} + +async function getGeneratedTscircuitDocs(): Promise { + if (generatedTscircuitDocsCache !== undefined) { + return generatedTscircuitDocsCache + } + + const generatedDocs = cleanMarkdownDoc( + await fetchOptionalFileContent(GENERATED_TSCIRCUIT_DOCS_URL), + ) + + if (generatedDocs) { + generatedTscircuitDocsCache = generatedDocs } + + return generatedDocs +} + +export function resetGeneratedTscircuitDocsCacheForTests() { + generatedTscircuitDocsCache = undefined } export const createLocalCircuitPrompt = async () => { @@ -33,10 +67,10 @@ 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(), + ]) const cleanedPropsDoc = propsDoc .split("\n") @@ -44,11 +78,20 @@ export const createLocalCircuitPrompt = async () => { .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: diff --git a/tests/prompt-templates/create-local-circuit-prompt.test.ts b/tests/prompt-templates/create-local-circuit-prompt.test.ts new file mode 100644 index 0000000..5351525 --- /dev/null +++ b/tests/prompt-templates/create-local-circuit-prompt.test.ts @@ -0,0 +1,98 @@ +import { afterEach, beforeEach, describe, expect, test } from "bun:test" +import { + createLocalCircuitPrompt, + resetGeneratedTscircuitDocsCacheForTests, +} from "../../lib/prompt-templates/create-local-circuit-prompt" + +const originalFetch = globalThis.fetch + +const propsDoc = `# Component Types + + +` + +function mockFetch(handler: (url: string) => Response | Promise) { + globalThis.fetch = (async (input) => { + const url = + typeof input === "string" + ? input + : input instanceof URL + ? input.toString() + : input.url + return handler(url) + }) as typeof fetch +} + +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 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 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(" { + 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) + }) +}) diff --git a/tests/tscircuitCoder.test.ts b/tests/tscircuitCoder.test.ts index d66c022..5b95e7a 100644 --- a/tests/tscircuitCoder.test.ts +++ b/tests/tscircuitCoder.test.ts @@ -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) + }, +) diff --git a/tests/utils/generate-random-prompts.test.ts b/tests/utils/generate-random-prompts.test.ts index 41a061c..fcc9ea4 100644 --- a/tests/utils/generate-random-prompts.test.ts +++ b/tests/utils/generate-random-prompts.test.ts @@ -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) From 6a998a95070232503dafff0281db3fa928fd25d2 Mon Sep 17 00:00:00 2001 From: DevvoZA <101029583+DevvoZA@users.noreply.github.com> Date: Sat, 23 May 2026 23:10:17 -0400 Subject: [PATCH 2/2] Bound optional generated docs fetch --- .../create-local-circuit-prompt.ts | 45 +++++++++++++++---- .../create-local-circuit-prompt.test.ts | 33 ++++++++++++++ 2 files changed, 70 insertions(+), 8 deletions(-) diff --git a/lib/prompt-templates/create-local-circuit-prompt.ts b/lib/prompt-templates/create-local-circuit-prompt.ts index fe5a7b8..6a51520 100644 --- a/lib/prompt-templates/create-local-circuit-prompt.ts +++ b/lib/prompt-templates/create-local-circuit-prompt.ts @@ -7,11 +7,16 @@ import { 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): Promise { - const response = await fetch(url) +async function fetchFileContent( + url: string, + init?: RequestInit, +): Promise { + const response = await fetch(url, init) if (!response.ok) { throw new Error( `Failed to fetch file: ${response.status} ${response.statusText}`, @@ -20,12 +25,30 @@ async function fetchFileContent(url: string): Promise { return await response.text() } -async function fetchOptionalFileContent(url: string): Promise { +async function fetchOptionalFileContent( + url: string, + timeoutMs: number, +): Promise { + const abortController = new AbortController() + let timeoutId: ReturnType | undefined + try { - return await fetchFileContent(url) + return await Promise.race([ + fetchFileContent(url, { signal: abortController.signal }), + new Promise((resolve) => { + timeoutId = setTimeout(() => { + abortController.abort() + resolve("") + }, timeoutMs) + }), + ]) } catch (error) { console.warn(`Optional prompt docs unavailable: ${url}`, error) return "" + } finally { + if (timeoutId !== undefined) { + clearTimeout(timeoutId) + } } } @@ -39,18 +62,24 @@ async function getGeneratedTscircuitDocs(): Promise { } const generatedDocs = cleanMarkdownDoc( - await fetchOptionalFileContent(GENERATED_TSCIRCUIT_DOCS_URL), + await fetchOptionalFileContent( + GENERATED_TSCIRCUIT_DOCS_URL, + generatedTscircuitDocsTimeoutMs, + ), ) - if (generatedDocs) { - generatedTscircuitDocsCache = generatedDocs - } + 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 () => { diff --git a/tests/prompt-templates/create-local-circuit-prompt.test.ts b/tests/prompt-templates/create-local-circuit-prompt.test.ts index 5351525..b92c582 100644 --- a/tests/prompt-templates/create-local-circuit-prompt.test.ts +++ b/tests/prompt-templates/create-local-circuit-prompt.test.ts @@ -2,6 +2,7 @@ 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 @@ -23,6 +24,8 @@ function mockFetch(handler: (url: string) => Response | Promise) { }) as typeof fetch } +const wait = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)) + beforeEach(() => { resetGeneratedTscircuitDocsCacheForTests() }) @@ -95,4 +98,34 @@ describe("createLocalCircuitPrompt", () => { 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) + }) })