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
49 changes: 42 additions & 7 deletions lib/prompt-templates/create-local-circuit-prompt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,16 @@ import {
fp,
} from "@tscircuit/footprinter"

async function fetchFileContent(url: string): Promise<string> {
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<string> | null = null

async function fetchFileContent(
url: string,
{ logErrors = true }: { logErrors?: boolean } = {},
): Promise<string> {
try {
const response = await fetch(url)
if (!response.ok) {
Expand All @@ -14,11 +23,25 @@ async function fetchFileContent(url: string): Promise<string> {
}
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<string> {
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()
Expand All @@ -33,23 +56,35 @@ 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")
.filter((line) => !line.startsWith("#"))
.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:

Expand Down
99 changes: 99 additions & 0 deletions tests/create-local-circuit-prompt.test.ts
Original file line number Diff line number Diff line change
@@ -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
}
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 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()
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"

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)
Expand Down
Loading