← Cloudflare Workers AI / workers-ai / guides / tutorials
Používání BigQuery s Workers AI
Nejjednodušší způsob, jak začít s Workers AI je vyzkoušet ho v Multimodální Playground ↗ a LLM playground ↗. Pokud se rozhodnete integrovat svůj kód s Workers AI, můžete následně využít jeho Endpointy REST API nebo Worker binding.
Co ale data? Co když chcete, aby tyto modely zpracovávaly data uložená mimo Cloudflare?
V tomto tutoriálu se naučíte, jak přenést data z Google BigQuery do Cloudflare Workeru, aby je bylo možné použít jako vstup pro modely Workers AI.
Předpoklady
Budete potřebovat:
- A Cloudflare Worker projekt, ve kterém běží Skript Hello World.
- Google Cloud Platform účet služby ↗ s přidružený klíč ↗ staženého souboru, který má přístup ke čtení v BigQuery.
- Přístup k tabulce BigQuery s testovacími daty, který vám umožní vytvořit BigQuery Job Query ↗. Pro tento tutoriál doporučujeme vytvořit si vlastní tabulku jako vzorkované tabulky ↗, pokud je nezklonujete do vlastního jmenného prostoru GCP, neumožní vám proti nim spouštět job queries. V tomto příkladu Korpus Hacker News ↗ byl použit na základě licence MIT.
1. Nastavte Cloudflare Worker
Chcete-li importovat data do Cloudflare a předat je Workers AI, použijete Cloudflare Worker. Pokud jej ještě nemáte vytvořený, projděte si prosím naši tutoriál o tom, jak začít.
Po provedení kroků pro vytvoření Workeru byste měli mít v novém projektu Workeru následující kód:
export default {
async fetch(request, env, ctx) {
return new Response("Hello World!");
},
};Po úspěšném vytvoření projektu Workeru byste také měli být schopni spustit npx wrangler dev v konzoli pro místní spuštění Workera:
[wrangler:inf] Ready on http://localhost:8787Otevřete kartu prohlížeče na adrese http://localhost:8787/ a zobrazíte si svého nasazeného Workera. Mějte na paměti, že port 8787 se ve vašem případě může lišit.
Nyní byste měli vidět Hello World! v prohlížeči:
Hello World!Pokud v tomto kroku narazíte na potíže, projděte si Úvodní příručka pro Workers.
2. Naimportujte klíč služby GCP do Workeru jako Secrets
Jakmile ověříte, že se Worker úspěšně vytvořil, budete muset odkázat na servisní klíč Google Cloud Platform vytvořený v Předpoklady sekci tohoto tutoriálu.
Stažený soubor s klíčem JSON z Google Cloud Platform by měl mít následující formát:
{
"type": "service_account",
"project_id": "<your_project_id>",
"private_key_id": "<your_private_key_id>",
"private_key": "<your_private_key>",
"client_email": "<your_service_account_id>@<your_project_id>.iam.gserviceaccount.com",
"client_id": "<your_oauth2_client_id>",
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
"token_uri": "https://oauth2.googleapis.com/token",
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
"client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/<your_service_account_id>%40<your_project_id>.iam.gserviceaccount.com",
"universe_domain": "googleapis.com"
}Pro tento tutoriál budete potřebovat pouze hodnoty následujících polí: client_email, private_key, private_key_id, a project_id.
Místo ukládání těchto informací jako prostého textu ve Workeru použijete Tajné klíče aby byl jeho nešifrovaný obsah dostupný pouze prostřednictvím samotného Workeru.
Importujte tyto tři hodnoty z JSON souboru do Secrets, počínaje polem z JSON klíčového souboru nazvaným client_email, který nyní zavoláme BQ_CLIENT_EMAIL (můžete použít i jiný název proměnné):
npx wrangler secret put BQ_CLIENT_EMAILBudete vyzváni k zadání tajné hodnoty, kterou bude hodnota pole client_email v souboru s JSON klíčem.
Pokud se secret úspěšně nahraje, zobrazí se následující zpráva:
✨ Success! Uploaded secret BQ_CLIENT_EMAILNyní naimportujte secrets pro tři zbývající pole: private_key, private_key_id, a project_id jako BQ_PRIVATE_KEY, BQ_PRIVATE_KEY_ID, a BQ_PROJECT_ID v tomto pořadí:
npx wrangler secret put BQ_PRIVATE_KEYnpx wrangler secret put BQ_PRIVATE_KEY_IDnpx wrangler secret put BQ_PROJECT_IDV tuto chvíli jste úspěšně naimportovali tři pole ze souboru s klíčem JSON staženého z Google Cloud Platform do Cloudflare Secrets, aby je bylo možné použít ve Workeru.
Tajné klíče jsou Workers dostupné až po nasazení. Aby byly dostupné i během vývoje, vytvořte .dev.vars soubor k lokálnímu uložení těchto přihlašovacích údajů a jejich použití formou proměnných prostředí.
Váš dev.vars soubor by měl vypadat následovně:
BQ_CLIENT_EMAIL="<your_service_account_id>@<your_project_id>.iam.gserviceaccount.com"
BQ_CLIENT_KEY="-----BEGIN PRIVATE KEY-----<content_of_your_private_key>-----END PRIVATE KEY-----\n"
BQ_PRIVATE_KEY_ID="<your_private_key_id>"
BQ_PROJECT_ID="<your_project_id>"Nezapomeňte zahrnout .dev.vars ve vašem projektu .gitignore soubor, aby se vaše přihlašovací údaje nenahrály do repozitáře při použití verzovacího systému.
Zkontrolujte, že se secrets správně načetly v src/index.js zaznamenáním jejich hodnot do konzole, například takto:
export default {
async fetch(request, env, ctx) {
console.log("BQ_CLIENT_EMAIL: ", env.BQ_CLIENT_EMAIL);
console.log("BQ_PRIVATE_KEY: ", env.BQ_PRIVATE_KEY);
console.log("BQ_PRIVATE_KEY_ID: ", env.BQ_PRIVATE_KEY_ID);
console.log("BQ_PROJECT_ID: ", env.BQ_PROJECT_ID);
return new Response("Hello World!");
},
};Restartujte Worker a spusťte npx wrangler dev. Měli byste vidět, že server nyní zmiňuje nově přidané proměnné:
Using vars defined in .dev.vars
Your worker has access to the following bindings:
- Vars:
- BQ_CLIENT_EMAIL: "(hidden)"
- BQ_PRIVATE_KEY: "(hidden)"
- BQ_PRIVATE_KEY_ID: "(hidden)"
- BQ_PROJECT_ID: "(hidden)"
[wrangler:inf] Ready on http://localhost:8787Pokud otevřete http://localhost:8787 v prohlížeči byste měli v konzoli vidět hodnoty proměnných, kde npx wrangler dev příkaz běží, přičemž stále vidíte pouze Hello World! text v okně prohlížeče.
Nyní máte z Workeru přístup k přihlašovacím údajům GCP. Dále nainstalujete knihovnu, která pomůže s vytvořením JSON Web Tokenu potřebného pro komunikaci s API GCP.
3. Nainstalujte knihovnu pro práci s JWT
Chcete-li pracovat s REST API BigQuery, budete si muset vygenerovat JSON Web Token ↗ k ověření svých požadavků pomocí přihlašovacích údajů, které jste v předchozím kroku uložili do Worker secrets.
V tomto tutoriálu použijete jose ↗ knihovna pro operace související s JWT. Nainstalujte ji spuštěním následujícího příkazu v konzoli:
npm i joseChcete-li ověřit, že instalace proběhla úspěšně, můžete spustit npm list, který vypisuje všechny nainstalované balíčky, abyste zkontrolovali, zda jose závislost byla přidána:
<project_name>@0.0.0
/<path_to_your_project>/<project_name>
├── @cloudflare/[email protected]
├── [email protected]
├── [email protected]
└── [email protected]4. Vygenerujte JSON web token
Nyní, když máte nainstalovanou knihovnu jose knihovnu, je čas ji importovat a přidat do kódu funkci, která vygeneruje podepsaný JSON Web Token (JWT):
import * as jose from 'jose';
...
const generateBQJWT = async (aCryptoKey, env) => {
const algorithm = "RS256";
const audience = "https://bigquery.googleapis.com/";
const expiryAt = (new Date().valueOf() / 1000);
const privateKey = await jose.importPKCS8(env.BQ_PRIVATE_KEY, algorithm);
// Generate signed JSON Web Token (JWT)
return new jose.SignJWT()
.setProtectedHeader({
typ: 'JWT',
alg: algorithm,
kid: env.BQ_PRIVATE_KEY_ID
})
.setIssuer(env.BQ_CLIENT_EMAIL)
.setSubject(env.BQ_CLIENT_EMAIL)
.setAudience(audience)
.setExpirationTime(expiryAt)
.setIssuedAt()
.sign(privateKey)
}
export default {
async fetch(request, env, ctx) {
...
// Create JWT to authenticate the BigQuery API call
let bqJWT;
try {
bqJWT = await generateBQJWT(env);
} catch (e) {
return new Response('An error has occurred while generating the JWT', { status: 500 })
}
},
...
};Nyní, když máte vytvořený JWT, je čas provést volání API na BigQuery a načíst nějaká data.
5. Odešlete ověřené požadavky do Google BigQuery
S JWT tokenem vytvořeným v předchozím kroku odešlete požadavek na API BigQuery a načtěte data z tabulky.
Nyní se dotážete na tabulku, kterou jste dříve v tomto tutoriálu vytvořili v BigQuery. Tento příklad používá vzorkovanou verzi Korpus Hacker News ↗ který byl použit na základě licence MIT a nahrán do BigQuery.
const queryBQ = async (bqJWT, path) => {
const bqEndpoint = `https://bigquery.googleapis.com${path}`
// In this example, text is a field in the BigQuery table that is being queried (hn.news_sampled)
const query = 'SELECT text FROM hn.news_sampled LIMIT 3';
const response = await fetch(bqEndpoint, {
method: "POST",
body: JSON.stringify({
"query": query
}),
headers: {
Authorization: `Bearer ${bqJWT}`
}
})
return response.json()
}
...
export default {
async fetch(request, env, ctx) {
...
let ticketInfo;
try {
ticketInfo = await queryBQ(bqJWT);
} catch (e) {
return new Response('An error has occurred while querying BQ', { status: 500 });
}
...
},
};Díky syrovým datům řádků z BigQuery je teď můžete naformátovat ve stylu podobném JSON.
6. Zformátujte výsledky dotazu
Jakmile načtete data z BigQuery, odpověď API BigQuery by měla vypadat přibližně takto:
{
...
"schema": {
"fields": [
{
"name": "title",
"type": "STRING",
"mode": "NULLABLE"
},
{
"name": "text",
"type": "STRING",
"mode": "NULLABLE"
}
]
},
...
"rows": [
{
"f": [
{
"v": "<some_value>"
},
{
"v": "<some_value>"
}
]
},
{
"f": [
{
"v": "<some_value>"
},
{
"v": "<some_value>"
}
]
},
{
"f": [
{
"v": "<some_value>"
},
{
"v": "<some_value>"
}
]
}
],
...
}Tento formát může být při procházení výsledků obtížně čitelný a pro práci nepraktický. Proto nyní implementujete funkci, která namapuje schéma na jednotlivé hodnoty, díky čemuž bude výsledný výstup přehlednější, jak je vidět níže. Každý řádek odpovídá jednomu objektu v poli.
[
{
title: "<some_value>",
text: "<some_value>",
},
{
title: "<some_value>",
text: "<some_value>",
},
{
title: "<some_value>",
text: "<some_value>",
},
];Vytvořte formatRows funkce, která přebírá počet řádků a polí vrácených v těle odpovědi BigQuery a vrací pole výsledků jako objekty s pojmenovanými poli.
const formatRows = (rowsWithoutFieldNames, fields) => {
// Index to fieldName
const fieldsByIndex = new Map();
// Load all fields by name and have their index in the array result as their key
fields.forEach((field, index) => {
fieldsByIndex.set(index, field.name)
})
// Iterate through rows
const rowsWithFieldNames = rowsWithoutFieldNames.map(row => {
// Per each row represented by an array f, iterate through the unnamed values and find their field names by searching them in the fieldsByIndex.
let newRow = {}
row.f.forEach((field, index) => {
const fieldName = fieldsByIndex.get(index);
if (fieldName) {
// For every field in a row, add them to newRow
newRow = ({ ...newRow, [fieldName]: field.v });
}
})
return newRow
})
return rowsWithFieldNames
}
export default {
async fetch(request, env, ctx) {
...
// Transform output format into array of objects with named fields
let formattedResults;
if ('rows' in ticketInfo) {
formattedResults = formatRows(ticketInfo.rows, ticketInfo.schema.fields);
console.log(formattedResults)
} else if ('error' in ticketInfo) {
return new Response(ticketInfo.error.message, { status: 500 })
}
...
},
};7. Vložte data do Workers AI
Jakmile jste odpověď z API BigQuery převedli na pole výsledků, vygenerujte pomocí LLM štítky a přiřaďte k nim skóre sentimentu prostřednictvím Workers AI:
const generateTags = (data, env) => {
return env.AI.run("@cf/meta/llama-3.1-8b-instruct", {
prompt: `Create three one-word tags for the following text. return only these three tags separated by a comma. don't return text that is not a category.Lowercase only. ${JSON.stringify(data)}`,
});
}
const generateSentimentScore = (data, env) => {
return env.AI.run("@cf/meta/llama-3.1-8b-instruct", {
prompt: `return a float number between 0 and 1 measuring the sentiment of the following text. 0 being negative and 1 positive. return only the number, no text. ${JSON.stringify(data)}`,
});
}
// Iterates through values, sends them to an AI handler and encapsulates all responses into a single Promise
const getAIGeneratedContent = (data, env, aiHandler) => {
let results = data?.map(dataPoint => {
return aiHandler(dataPoint, env)
})
return Promise.all(results)
}
...
export default {
async fetch(request, env, ctx) {
...
let summaries, sentimentScores;
try {
summaries = await getAIGeneratedContent(formattedResults, env, generateTags);
sentimentScores = await getAIGeneratedContent(formattedResults, env, generateSentimentScore)
} catch {
return new Response('There was an error while generating the text summaries or sentiment scores')
}
},
formattedResults = formattedResults?.map((formattedResult, i) => {
if (sentimentScores[i].response && summaries[i].response) {
return {
...formattedResult,
'sentiment': parseFloat(sentimentScores[i].response).toFixed(2),
'tags': summaries[i].response.split(',').map((result) => result.trim())
}
}
}
};V souboru Wrangler ve svém projektu odkomentujte následující řádky:
{
"ai": {
"binding": "AI"
}
}[ai]
binding = "AI"Restartujte Worker, který běží lokálně, a poté přejděte na endpoint vaší aplikace:
curl http://localhost:8787Pravděpodobně budete vyzváni k přihlášení do svého účtu Cloudflare a k udělení dočasného přístupu nástroji Wrangler (Cloudflare CLI), aby mohl při používání Worker AI použít váš účet.
Jakmile otevřete http://localhost:8787 byste měli vidět výstup podobný tomuto:
{
"data": [
{
"text": "You can see a clear spike in submissions right around US Thanksgiving.",
"sentiment": "0.61",
"tags": [
"trends",
"submissions",
"thanksgiving"
]
},
{
"text": "I didn't test the changes before I published them. I basically did development on the running server. In fact for about 30 seconds the comments page was broken due to a bug.",
"sentiment": "0.35",
"tags": [
"software",
"deployment",
"error"
]
},
{
"text": "I second that. As I recall, it's a very enjoyable 700-page brain dump by someone who's really into his subject. The writing has a personal voice; there are lots of asides, dry wit, and typos that suggest restrained editing. The discussion is intelligent and often theoretical (and Bartle is not scared to use mathematical metaphors), but the tone is not academic.",
"sentiment": "0.86",
"tags": [
"review",
"game",
"design"
]
}
]
}Konkrétní hodnoty a pole budou většinou záviset na dotazu z kroku 5, který se následně předá do LLM.
Výsledek
Veškerý kód uvedený v jednotlivých krocích je spojen do následujícího kódu v src/index.js:
import * as jose from "jose";
const generateBQJWT = async (env) => {
const algorithm = "RS256";
const audience = "https://bigquery.googleapis.com/";
const expiryAt = new Date().valueOf() / 1000;
const privateKey = await jose.importPKCS8(env.BQ_PRIVATE_KEY, algorithm);
// Generate signed JSON Web Token (JWT)
return new jose.SignJWT()
.setProtectedHeader({
typ: "JWT",
alg: algorithm,
kid: env.BQ_PRIVATE_KEY_ID,
})
.setIssuer(env.BQ_CLIENT_EMAIL)
.setSubject(env.BQ_CLIENT_EMAIL)
.setAudience(audience)
.setExpirationTime(expiryAt)
.setIssuedAt()
.sign(privateKey);
};
const queryBQ = async (bgJWT, path) => {
const bqEndpoint = `https://bigquery.googleapis.com${path}`;
const query = "SELECT text FROM hn.news_sampled LIMIT 3";
const response = await fetch(bqEndpoint, {
method: "POST",
body: JSON.stringify({
query: query,
}),
headers: {
Authorization: `Bearer ${bgJWT}`,
},
});
return response.json();
};
const formatRows = (rowsWithoutFieldNames, fields) => {
// Index to fieldName
const fieldsByIndex = new Map();
fields.forEach((field, index) => {
fieldsByIndex.set(index, field.name);
});
const rowsWithFieldNames = rowsWithoutFieldNames.map((row) => {
// Map rows into an array of objects with field names
let newRow = {};
row.f.forEach((field, index) => {
const fieldName = fieldsByIndex.get(index);
if (fieldName) {
newRow = { ...newRow, [fieldName]: field.v };
}
});
return newRow;
});
return rowsWithFieldNames;
};
const generateTags = (data, env) => {
return env.AI.run("@cf/meta/llama-3.1-8b-instruct", {
prompt: `Create three one-word tags for the following text. return only these three tags separated by a comma. don't return text that is not a category.Lowercase only. ${JSON.stringify(data)}`,
});
};
const generateSentimentScore = (data, env) => {
return env.AI.run("@cf/meta/llama-3.1-8b-instruct", {
prompt: `return a float number between 0 and 1 measuring the sentiment of the following text. 0 being negative and 1 positive. return only the number, no text. ${JSON.stringify(data)}`,
});
};
const getAIGeneratedContent = (data, env, aiHandler) => {
let results = data?.map((dataPoint) => {
return aiHandler(dataPoint, env);
});
return Promise.all(results);
};
export default {
async fetch(request, env, ctx) {
// Create JWT to authenticate the BigQuery API call
let bqJWT;
try {
bqJWT = await generateBQJWT(env);
} catch (error) {
console.log(error);
return new Response("An error has occurred while generating the JWT", {
status: 500,
});
}
// Fetch results from BigQuery
let ticketInfo;
try {
ticketInfo = await queryBQ(
bqJWT,
`/bigquery/v2/projects/${env.BQ_PROJECT_ID}/queries`,
);
} catch (error) {
console.log(error);
return new Response("An error has occurred while querying BQ", {
status: 500,
});
}
// Transform output format into array of objects with named fields
let formattedResults;
if ("rows" in ticketInfo) {
formattedResults = formatRows(ticketInfo.rows, ticketInfo.schema.fields);
} else if ("error" in ticketInfo) {
return new Response(ticketInfo.error.message, { status: 500 });
}
// Generate AI summaries and sentiment scores
let summaries, sentimentScores;
try {
summaries = await getAIGeneratedContent(
formattedResults,
env,
generateTags,
);
sentimentScores = await getAIGeneratedContent(
formattedResults,
env,
generateSentimentScore,
);
} catch {
return new Response(
"There was an error while generating the text summaries or sentiment scores",
);
}
// Add AI summaries and sentiment scores to previous results
formattedResults = formattedResults?.map((formattedResult, i) => {
if (sentimentScores[i].response && summaries[i].response) {
return {
...formattedResult,
sentiment: parseFloat(sentimentScores[i].response).toFixed(2),
tags: summaries[i].response.split(",").map((result) => result.trim()),
};
}
});
const response = { data: formattedResults };
return new Response(JSON.stringify(response), {
headers: { "Content-Type": "application/json" },
});
},
};Pokud chcete tento Worker nasadit, můžete to udělat spuštěním npx wrangler deploy:
Total Upload: <size_of_your_worker> KiB / gzip: <compressed_size_of_your_worker> KiB
Uploaded <name_of_your_worker> (x sec)
Deployed <name_of_your_worker> triggers (x sec)
https://<your_public_worker_endpoint>
Current Version ID: <worker_script_version_id>Tím vytvoříte veřejný endpoint, přes který lze k Workeru přistupovat odkudkoli. Pokud pracujete s produkčními daty, mějte to na paměti a zajistěte doplňkové řízení přístupu.
Závěr
V tomto tutoriálu jste se naučili, jak integrovat Google BigQuery a Cloudflare Workers vytvořením klíče GCP service account a uložením jeho části jako Worker secrets. Ten byl následně importován do kódu a pomocí jose npm knihovny jste vytvořili JSON Web Token pro ověření API dotazu vůči BigQuery.
Jakmile jste výsledky získali, naformátovali jste je tak, aby je bylo možné předat generativním AI modelům prostřednictvím Workers AI, a vygenerovali jste štítky a provedli analýzu sentimentu extrahovaných dat.
Další kroky
Pokud váš workflow místo zobrazení výsledků zpracování dat AI modelem v prohlížeči vyžaduje jejich načtení a uložení (například do R2 nebo D1) v pravidelných intervalech, budete možná chtít přidat naplánovaný handler pro tohoto Workera. Díky tomu můžete Workera spouštět v předem definovaném intervalu pomocí Cron Trigger. Doporučujeme prostudovat si diagramy referenční architektury na Načítání dat z BigQuery do Workers AI.
Jedním z případů použití importu dat z jiných zdrojů, jako jste to udělali v tomto tutoriálu, je vytvoření systému RAG. Pokud je to pro vás relevantní, podívejte se na Tutoriál: Vytvořte AI s Retrieval Augmented Generation (RAG).
Přehled dalších modelů AI, které můžete na Cloudflare použít, najdete na Workers AI sekci naší dokumentace.