diff --git a/lib/prompt-templates/create-local-circuit-prompt.ts b/lib/prompt-templates/create-local-circuit-prompt.ts index a93f11f..82b881a 100644 --- a/lib/prompt-templates/create-local-circuit-prompt.ts +++ b/lib/prompt-templates/create-local-circuit-prompt.ts @@ -4,7 +4,16 @@ import { fp, } from "@tscircuit/footprinter" -async function fetchFileContent(url: string): Promise { +const COMPONENT_TYPES_URL = + "https://raw.githubusercontent.com/tscircuit/props/main/generated/COMPONENT_TYPES.md" +const GENERATED_TSCIRCUIT_DOCS_URL = "https://docs.tscircuit.com/ai.txt" + +let generatedDocsPromise: Promise | null = null + +async function fetchFileContent( + url: string, + { logErrors = true }: { logErrors?: boolean } = {}, +): Promise { try { const response = await fetch(url) if (!response.ok) { @@ -14,11 +23,25 @@ async function fetchFileContent(url: string): Promise { } return await response.text() } catch (error) { - console.error("Error fetching file content:", error) + if (logErrors) { + console.error("Error fetching file content:", error) + } throw error } } +async function fetchOptionalGeneratedDocs(): Promise { + generatedDocsPromise ??= fetchFileContent(GENERATED_TSCIRCUIT_DOCS_URL, { + logErrors: false, + }).catch(() => "") + + return generatedDocsPromise +} + +export function __resetGeneratedDocsCacheForTests() { + generatedDocsPromise = null +} + export const createLocalCircuitPrompt = async () => { const footprintNamesByType = getFootprintNamesByType() const footprintSizes = getFootprintSizes() @@ -33,10 +56,10 @@ export const createLocalCircuitPrompt = async () => { "", ) - const propsDoc = - (await fetchFileContent( - "https://raw.githubusercontent.com/tscircuit/props/main/generated/COMPONENT_TYPES.md", - )) || "" + const [propsDoc, generatedDocs] = await Promise.all([ + fetchFileContent(COMPONENT_TYPES_URL), + fetchOptionalGeneratedDocs(), + ]) const cleanedPropsDoc = propsDoc .split("\n") @@ -44,12 +67,24 @@ export const createLocalCircuitPrompt = async () => { .join("\n") .replace(/\n\n+/g, "\n\n") + const generatedDocsSection = generatedDocs.trim() + ? `## Auto-generated tscircuit docs + +The following content comes from ${GENERATED_TSCIRCUIT_DOCS_URL}. Treat it as +the most current tscircuit documentation when it conflicts with the handwritten +overview below. + +${generatedDocs.trim()} + +` + : "" + 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 -## tscircuit API overview +${generatedDocsSection}## tscircuit API overview Here's an overview of the tscircuit API: diff --git a/tests/create-local-circuit-prompt.test.ts b/tests/create-local-circuit-prompt.test.ts new file mode 100644 index 0000000..4e6db30 --- /dev/null +++ b/tests/create-local-circuit-prompt.test.ts @@ -0,0 +1,99 @@ +import { afterEach, beforeEach, expect, test } from "bun:test" +import { + __resetGeneratedDocsCacheForTests, + createLocalCircuitPrompt, +} from "../lib/prompt-templates/create-local-circuit-prompt" + +const COMPONENT_TYPES_URL = + "https://raw.githubusercontent.com/tscircuit/props/main/generated/COMPONENT_TYPES.md" +const GENERATED_DOCS_URL = "https://docs.tscircuit.com/ai.txt" + +const originalFetch = globalThis.fetch + +beforeEach(() => { + __resetGeneratedDocsCacheForTests() +}) + +afterEach(() => { + globalThis.fetch = originalFetch + __resetGeneratedDocsCacheForTests() +}) + +test("includes generated docs in the local circuit system prompt", async () => { + const requestedUrls: string[] = [] + mockFetch({ + requestedUrls, + generatedDocs: "generated-docs-sentinel: prefer modern tscircuit APIs", + propsDoc: "# Component Types\n\n## resistor\n\nresistance prop", + }) + + const prompt = await createLocalCircuitPrompt() + + expect(requestedUrls).toContain(GENERATED_DOCS_URL) + expect(requestedUrls).toContain(COMPONENT_TYPES_URL) + expect(prompt).toContain("## Auto-generated tscircuit docs") + expect(prompt).toContain("generated-docs-sentinel") + expect(prompt.indexOf("generated-docs-sentinel")).toBeLessThan( + prompt.indexOf("## tscircuit API overview"), + ) + expect(prompt).toContain("resistance prop") +}) + +test("still builds the prompt when generated docs are unavailable", async () => { + mockFetch({ + generatedDocsStatus: 503, + propsDoc: "# Component Types\n\n## capacitor\n\ncapacitance prop", + }) + + const prompt = await createLocalCircuitPrompt() + + expect(prompt).not.toContain("## Auto-generated tscircuit docs") + expect(prompt).toContain("## tscircuit API overview") + expect(prompt).toContain("capacitance prop") +}) + +test("caches generated docs across prompt builds", async () => { + const requestedUrls: string[] = [] + mockFetch({ + requestedUrls, + generatedDocs: "cached generated docs", + propsDoc: "# Component Types\n\n## led\n\nled prop", + }) + + await createLocalCircuitPrompt() + await createLocalCircuitPrompt() + + expect(requestedUrls.filter((url) => url === GENERATED_DOCS_URL)).toHaveLength( + 1, + ) + expect(requestedUrls.filter((url) => url === COMPONENT_TYPES_URL)).toHaveLength( + 2, + ) +}) + +function mockFetch({ + requestedUrls, + generatedDocs = "", + generatedDocsStatus = 200, + propsDoc, +}: { + requestedUrls?: string[] + generatedDocs?: string + generatedDocsStatus?: number + propsDoc: string +}) { + globalThis.fetch = (async (input: string | URL | Request) => { + const url = input.toString() + requestedUrls?.push(url) + + if (url === GENERATED_DOCS_URL) { + return new Response(generatedDocs, { status: generatedDocsStatus }) + } + + if (url === COMPONENT_TYPES_URL) { + return new Response(propsDoc, { status: 200 }) + } + + return new Response(`Unexpected URL: ${url}`, { status: 500 }) + }) as typeof fetch +} diff --git a/tests/tscircuitCoder.test.ts b/tests/tscircuitCoder.test.ts index d66c022..9125144 100644 --- a/tests/tscircuitCoder.test.ts +++ b/tests/tscircuitCoder.test.ts @@ -1,8 +1,10 @@ -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 testWithOpenAiKey = process.env.OPENAI_API_KEY ? test : test.skip + +testWithOpenAiKey("TscircuitCoder submitPrompt streams and updates vfs", async () => { const streamedChunks: string[] = [] let vfsUpdated = false const tscircuitCoder = createTscircuitCoder() @@ -21,14 +23,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") diff --git a/tests/utils/generate-random-prompts.test.ts b/tests/utils/generate-random-prompts.test.ts index 41a061c..22cea28 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" describe("generateRandomPrompts", () => { - it("should return an array of prompts", async () => { + const itWithOpenAiKey = process.env.OPENAI_API_KEY ? it : it.skip + + itWithOpenAiKey("should return an array of prompts", async () => { const prompts = await generateRandomPrompts(3) expect(Array.isArray(prompts)).toBe(true)