-
-
Save simonw/7f8db0c452378eb4fa4747196b8194dc to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import Anthropic from "npm:@anthropic-ai/sdk@0.24.0"; | |
/* This automatically picks up the API key from the ANTHROPIC_API_KEY | |
environment variable, which we configured in the Val Town settings */ | |
const anthropic = new Anthropic(); | |
async function suggestKeywords(question) { | |
// Takes a question like "What is shot-scraper?" and asks 3.5 Sonnet | |
// to suggest individual search keywords to help answer the question. | |
const message = await anthropic.messages.create({ | |
max_tokens: 128, | |
model: "claude-3-5-sonnet-20240620", | |
// The tools option enforces a JSON schema array of strings | |
tools: [{ | |
name: "suggested_search_keywords", | |
description: "Suggest individual search keywords to help answer the question.", | |
input_schema: { | |
type: "object", | |
properties: { | |
keywords: { | |
type: "array", | |
items: { | |
type: "string", | |
}, | |
description: "List of suggested single word search keywords", | |
}, | |
}, | |
required: ["keywords"], | |
}, | |
}], | |
// This forces it to always run the suggested_search_keywords tool | |
tool_choice: { type: "tool", name: "suggested_search_keywords" }, | |
messages: [ | |
{ role: "user", content: question }, | |
], | |
}); | |
// This helped TypeScript complain less about accessing .input.keywords | |
// since it knows this object can be one of two different types | |
if (message.content[0].type == "text") { | |
throw new Error(message.content[0].text); | |
} | |
return message.content[0].input.keywords; | |
} | |
// The SQL query from earlier | |
const sql = `select | |
blog_entry.id, | |
blog_entry.title, | |
blog_entry.body, | |
blog_entry.created | |
from | |
blog_entry | |
join blog_entry_fts on blog_entry_fts.rowid = blog_entry.rowid | |
where | |
blog_entry_fts match :search | |
order by | |
rank | |
limit | |
10`; | |
async function runSearch(keywords) { | |
// Turn the keywords into "word1" OR "word2" OR "word3" | |
const search = keywords.map(s => `"${s}"`).join(" OR "); | |
// Compose the JSON API URL to run the query | |
const params = new URLSearchParams({ | |
search, | |
sql, | |
_shape: "array", | |
}); | |
const url = "https://datasette.simonwillison.net/simonwillisonblog.json?" + params; | |
const result = await (await fetch(url)).json(); | |
return result; | |
} | |
export default async function(req: Request) { | |
// This is the Val Town HTTP handler | |
const url = new URL(req.url); | |
const question = url.searchParams.get("question").slice(0, 40); | |
if (!question) { | |
return Response.json({ "error": "No question provided" }); | |
} | |
// Turn the question into search terms | |
const keywords = await suggestKeywords(question); | |
// Run the actual search | |
const result = await runSearch(keywords); | |
// Strip HTML tags from each body property, modify in-place: | |
result.forEach(r => { | |
r.body = r.body.replace(/<[^>]*>/g, ""); | |
}); | |
// Glue together a string of the title and body properties in one go | |
const context = result.map(r => r.title + " " + r.body).join("\n\n"); | |
// Initially we asked for an answer, now we ask for a whole HTML document | |
const message = await anthropic.messages.create({ | |
max_tokens: 1024, | |
model: "claude-3-5-sonnet-20240620", // "claude-3-haiku-20240307", | |
system: "Return a full HTML document as your answer, no markdown, make it pretty with exciting relevant CSS", | |
messages: [ | |
{ role: "user", content: context }, | |
{ role: "assistant", content: "Thank you for the context, I am ready to answer your question as HTML" }, | |
{ role: "user", content: question }, | |
], | |
}); | |
// Return back whatever HTML Claude gave us | |
return new Response(message.content[0].text, { | |
status: 200, | |
headers: { "Content-Type": "text/html" } | |
}); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment