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
58 changes: 53 additions & 5 deletions lib/prompt-templates/create-local-circuit-prompt.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
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 generatedDocsCache: string | undefined

async function fetchFileContent(url: string): Promise<string> {
try {
const response = await fetch(url)
Expand All @@ -19,6 +25,36 @@ async function fetchFileContent(url: string): Promise<string> {
}
}

async function fetchOptionalFileContent(url: string): Promise<string> {
try {
const response = await fetch(url)
if (!response.ok) {
return ""
}
return await response.text()
} catch {
return ""
}
}

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

const generatedDocs = await fetchOptionalFileContent(
GENERATED_TSCIRCUIT_DOCS_URL,
)
if (generatedDocs.trim()) {
generatedDocsCache = generatedDocs
}
return generatedDocs
}

export function clearCreateLocalCircuitPromptCacheForTests() {
generatedDocsCache = undefined
}

export const createLocalCircuitPrompt = async () => {
const footprintNamesByType = getFootprintNamesByType()
const footprintSizes = getFootprintSizes()
Expand All @@ -33,22 +69,34 @@ 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_DOC_URL),
getGeneratedTscircuitDocs(),
])

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

const generatedDocsSection = generatedDocs.trim()
? `## Auto-generated tscircuit docs

The following generated documentation is the most up-to-date tscircuit API reference. Prefer it over older hand-written examples when they disagree.

<tscircuit_generated_docs>
${generatedDocs}
</tscircuit_generated_docs>`
: ""

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

${generatedDocsSection}

## tscircuit API overview

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

const originalFetch = globalThis.fetch

beforeEach(() => {
clearCreateLocalCircuitPromptCacheForTests()
})

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

describe("createLocalCircuitPrompt", () => {
it("includes generated tscircuit docs and caches them across prompt builds", async () => {
const fetchCounts = new Map<string, number>()

globalThis.fetch = (async (input: RequestInfo | URL) => {
const url = input.toString()
fetchCounts.set(url, (fetchCounts.get(url) ?? 0) + 1)

if (url === "https://docs.tscircuit.com/ai.txt") {
return new Response("GENERATED_TSCIRCUIT_DOCS", { status: 200 })
}

if (
url ===
"https://raw.githubusercontent.com/tscircuit/props/main/generated/COMPONENT_TYPES.md"
) {
return new Response("# Props\n\nPROP_COMPONENT_DOCS", { status: 200 })
}

throw new Error(`Unexpected URL: ${url}`)
}) as typeof fetch

const firstPrompt = await createLocalCircuitPrompt()
const secondPrompt = await createLocalCircuitPrompt()

expect(firstPrompt).toContain("## Auto-generated tscircuit docs")
expect(firstPrompt).toContain("<tscircuit_generated_docs>")
expect(firstPrompt).toContain("GENERATED_TSCIRCUIT_DOCS")
expect(firstPrompt).toContain("PROP_COMPONENT_DOCS")
expect(secondPrompt).toContain("GENERATED_TSCIRCUIT_DOCS")
expect(fetchCounts.get("https://docs.tscircuit.com/ai.txt")).toBe(1)
})

it("keeps building the prompt if generated docs are unavailable", async () => {
let generatedDocsFetchCount = 0

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

if (url === "https://docs.tscircuit.com/ai.txt") {
generatedDocsFetchCount += 1
return new Response("", { status: 503 })
}

if (
url ===
"https://raw.githubusercontent.com/tscircuit/props/main/generated/COMPONENT_TYPES.md"
) {
return new Response("# Props\n\nPROP_COMPONENT_DOCS", { status: 200 })
}

throw new Error(`Unexpected URL: ${url}`)
}) as typeof fetch

const prompt = await createLocalCircuitPrompt()

expect(prompt).not.toContain("<tscircuit_generated_docs>")
expect(prompt).toContain("PROP_COMPONENT_DOCS")
expect(prompt).toContain("## tscircuit API overview")
expect(generatedDocsFetchCount).toBe(1)
})

it("does not cache empty generated docs after a transient fetch failure", async () => {
let generatedDocsFetchCount = 0

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

if (url === "https://docs.tscircuit.com/ai.txt") {
generatedDocsFetchCount += 1
if (generatedDocsFetchCount === 1) {
return new Response("", { status: 503 })
}
return new Response("RECOVERED_GENERATED_DOCS", { status: 200 })
}

if (
url ===
"https://raw.githubusercontent.com/tscircuit/props/main/generated/COMPONENT_TYPES.md"
) {
return new Response("# Props\n\nPROP_COMPONENT_DOCS", { status: 200 })
}

throw new Error(`Unexpected URL: ${url}`)
}) as typeof fetch

const firstPrompt = await createLocalCircuitPrompt()
const secondPrompt = await createLocalCircuitPrompt()

expect(firstPrompt).not.toContain("<tscircuit_generated_docs>")
expect(secondPrompt).toContain("RECOVERED_GENERATED_DOCS")
expect(generatedDocsFetchCount).toBe(2)
})
})
10 changes: 6 additions & 4 deletions tests/tscircuitCoder.test.ts
Original file line number Diff line number Diff line change
@@ -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 openAiTest = process.env.RUN_OPENAI_TESTS ? test : test.skip

openAiTest("TscircuitCoder submitPrompt streams and updates vfs", async () => {
const streamedChunks: string[] = []
let vfsUpdated = false
const tscircuitCoder = createTscircuitCoder()
Expand All @@ -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")

Expand Down
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 openAiIt = process.env.RUN_OPENAI_TESTS ? it : it.skip

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

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