Cloudflare Workers Cloudflare Workers Cloudflare Workers · Lab 5/7 Lab 5/7 Lab 5/7 · 20 phút · 20 min · 20 នាទី

05

Tóm tắt bằng Workers AI AI-Powered Summaries សង្ខេបដោយ Workers AI

Gắn Workers AI để tự tóm tắt mỗi bookmark khi lưu, ghi kết quả vào D1. Add Workers AI to automatically generate a summary for each bookmark when it is saved, storing the result in D1. ភ្ជាប់ Workers AI ដើម្បីសង្ខេប bookmark ស្វ័យប្រវត្តិពេលរក្សាទុក សរសេរលទ្ធផលទៅ D1។

Nội dung bước (lệnh, code) giữ nguyên tiếng Anh từ nguồn chính thức. Step body (commands, code) stays in English from the official source. ខ្លឹមសារជំហាន (ពាក្យបញ្ជា និង code) រក្សាភាសាអង់គ្លេសពីប្រភពផ្លូវការ។ labs.cloudflare.dev ↗

Cần trước Prerequisites តម្រូវការជាមុន

  • Đã xong bước 4 Completed Step 4 បានបញ្ចប់ជំហាន 4
  • bookmark-api đã có D1 và KV bookmark-api with D1 and KV bookmark-api មាន D1 និង KV

Bạn sẽ làm được Learning objectives គោលបំណងសិក្សា

  • Gắn model sinh text vào pipeline data có sẵn Integrate a text generation model into an existing data pipeline ភ្ជាប់ model បង្កើតអត្ថបទទៅ pipeline data មានស្រាប់
  • Thiết kế lời gọi AI fail mà không chặn thao tác lõi Design AI calls that fail gracefully without blocking core operations រចនាការហៅ AI ឲ្យ fail ដោយមិនទប់ប្រតិបត្តិការគន្លឹះ
  • Đánh giá khi nào infer ngay trong request hay chạy nền Evaluate when to run inference inline vs as a background task វាយតម្លៃពេលណា infer ក្នុង request ឬរត់ផ្ទៃក្រោយ
bash
npx wrangler dev --remote

Xem: Cloudflare Workers AI Watch: Cloudflare Workers AI មើល៖ Cloudflare Workers AI

This video covers the fundamentals of Workers AI.

This video covers the fundamentals of Workers AI.

This video covers the fundamentals of Workers AI.

Cloudflare Workers AI

Bước 1: Thêm AI binding Step 1: Add the AI Binding ជំហាន 1: បន្ថែម AI binding

Chúng ta đang xây What we're building អ្វីដែលយើងកំពុងសង់
Workers AI configured in your wrangler.jsonc alongside KV and D1.
Vì sao quan trọng Why this matters ហេតុអ្វីសំខាន់
The AI binding gives your Worker access to machine learning models with no API keys or external services.

Update wrangler.jsonc to add the AI binding:

Update wrangler.jsonc to add the AI binding:

Update wrangler.jsonc to add the AI binding:

json
{
  "name": "bookmark-api",
  "main": "src/index.ts",
  "compatibility_date": "2026-03-01",
  //...
  "ai": {
    "binding": "AI"
  }
}

Regenerate types:

Regenerate types:

Regenerate types:

bash
npx wrangler types

Bước 2: Thêm cột summary trên D1 Step 2: Add a Summary Column to D1 ជំហាន 2: បន្ថែមជួរឈរ summary លើ D1

Chúng ta đang xây What we're building អ្វីដែលយើងកំពុងសង់
A new `summary` column in the bookmarks table.
Vì sao quan trọng Why this matters ហេតុអ្វីសំខាន់
The AI-generated summary needs to be stored persistently alongside the bookmark.

Create migration-summary.sql:

Create migration-summary.sql:

Create migration-summary.sql:

sql
ALTER TABLE bookmarks ADD COLUMN summary TEXT DEFAULT '';

Apply the migration to the remote database (Workers AI requires --remote):

Apply the migration to the remote database (Workers AI requires --remote):

Apply the migration to the remote database (Workers AI requires --remote):

bash
npx wrangler d1 execute bookmark-db --remote --file=./migration-summary.sql

Bước 3: Cập nhật Worker Step 3: Update the Worker ជំហាន 3: ធ្វើបច្ចុប្បន្នភាព Worker

Chúng ta đang xây What we're building អ្វីដែលយើងកំពុងសង់
The createBookmark handler calls Workers AI to generate a summary before saving.
Vì sao quan trọng Why this matters ហេតុអ្វីសំខាន់
This demonstrates integrating AI into an existing data pipeline, the most practical way to use Workers AI.

The only function that changes is createBookmark. The rest of the file stays the same. Here is the updated src/index.ts:

The only function that changes is createBookmark. The rest of the file stays the same. Here is the updated src/index.ts:

The only function that changes is createBookmark. The rest of the file stays the same. Here is the updated src/index.ts:

typescript

interface Bookmark {
  id: string;
  url: string;
  title: string;
  tags: string;
  summary: string;    // NEW: AI-generated summary
  created_at: string;
}

export default {
  async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
    const url = new URL(request.url);
    const path = url.pathname;
    const method = request.method;

    if (path === '/bookmarks' && method === 'GET') {
      return listBookmarks(env, url);
    }

    if (path === '/bookmarks' && method === 'POST') {
      return createBookmark(request, env);
    }

    const match = path.match(/^\/bookmarks\/([a-zA-Z0-9_-]+)$/);
    if (match && method === 'GET') {
      return getBookmark(match[1], env);
    }

    if (match && method === 'DELETE') {
      return deleteBookmark(match[1], env);
    }

    if (path === '/') {
      return Response.json({
        name: 'Bookmark API',
        version: '4.0.0',
        storage: 'D1 + KV cache',
        ai: 'Workers AI (auto-summary)',
        endpoints: [
          'GET /bookmarks',
          'GET /bookmarks?tag=docs',
          'POST /bookmarks',
          'GET /bookmarks/:id',
          'DELETE /bookmarks/:id'
        ]
      });
    }

    return Response.json({ error: 'Not Found' }, { status: 404 });
  },
};

// NEW: generate a summary using Workers AI
async function generateSummary(title: string, url: string, env: Env): Promise<string> {
  try {
    const response = await env.AI.run('@cf/meta/llama-3.1-8b-instruct-fast', {
      messages: [
        {
          role: 'system',
          content: 'You are a helpful assistant that writes concise bookmark descriptions. Respond with exactly one sentence, no more than 20 words.'
        },
        {
          role: 'user',
          content: `Write a one-sentence description for this bookmark:\nTitle: ${title}\nURL: ${url}`
        }
      ]
    });

    // env.AI.run() returns an object like { response: "the text" }.
    // So response.response accesses the generated text. This is not a typo!
    return response.response?.trim() || '';
  } catch (error) {
    console.error('AI summary failed:', error);
    return ''; // Fail gracefully - bookmark is saved without summary
  }
}

// CHANGED: calls generateSummary before saving
async function createBookmark(request: Request, env: Env): Promise<Response> {
  let body: { url?: string; title?: string; tags?: string };

  try {
    body = await request.json() as { url?: string; title?: string; tags?: string };
  } catch {
    return Response.json(
      { error: 'Invalid JSON in request body' },
      { status: 400 }
    );
  }

  if (!body.url || !body.title) {
    return Response.json(
      { error: 'Missing required fields: url, title' },
      { status: 400 }
    );
  }

  const id = crypto.randomUUID().slice(0, 8);
  const tags = body.tags || '';

  // NEW: generate AI summary
  const summary = await generateSummary(body.title, body.url, env);

  // Write to D1 (now includes summary)
  const result = await env.DB.prepare(
    `INSERT INTO bookmarks (id, url, title, tags, summary)
     VALUES (?, ?, ?, ?, ?)
     RETURNING *`
  ).bind(id, body.url, body.title, tags, summary).first<Bookmark>();

  if (!result) {
    return Response.json({ error: 'Failed to create bookmark' }, { status: 500 });
  }

  // Cache in KV
  await env.BOOKMARKS.put(id, JSON.stringify(result), { expirationTtl: 3600 });

  return Response.json(result, { status: 201 });
}

// UNCHANGED from Step 4
async function listBookmarks(env: Env, url: URL): Promise<Response> {
  const tag = url.searchParams.get('tag');

  let results: Bookmark[];

  if (tag) {
    const { results: rows } = await env.DB.prepare(
      `SELECT * FROM bookmarks WHERE tags LIKE ? ORDER BY created_at DESC`
    ).bind(`%${tag}%`).all<Bookmark>();
    results = rows;
  } else {
    const { results: rows } = await env.DB.prepare(
      `SELECT * FROM bookmarks ORDER BY created_at DESC`
    ).all<Bookmark>();
    results = rows;
  }

  return Response.json({ bookmarks: results, count: results.length });
}

// UNCHANGED from Step 4
async function getBookmark(id: string, env: Env): Promise<Response> {
  const cached = await env.BOOKMARKS.get<Bookmark>(id, 'json');
  if (cached) {
    return Response.json({ ...cached, _cached: true });
  }

  const bookmark = await env.DB.prepare(
    'SELECT * FROM bookmarks WHERE id = ?'
  ).bind(id).first<Bookmark>();

  if (!bookmark) {
    return Response.json({ error: 'Bookmark not found' }, { status: 404 });
  }

  await env.BOOKMARKS.put(id, JSON.stringify(bookmark), { expirationTtl: 3600 });
  return Response.json(bookmark);
}

// UNCHANGED from Step 4
async function deleteBookmark(id: string, env: Env): Promise<Response> {
  const existing = await env.DB.prepare(
    'SELECT id FROM bookmarks WHERE id = ?'
  ).bind(id).first();

  if (!existing) {
    return Response.json({ error: 'Bookmark not found' }, { status: 404 });
  }

  await env.DB.prepare('DELETE FROM bookmarks WHERE id = ?').bind(id).run();
  await env.BOOKMARKS.delete(id);

  return Response.json({ message: 'Bookmark deleted' });
}

Bước 4: Test tóm tắt AI Step 4: Test AI Summaries ជំហាន 4: Test សង្ខេប AI

Chúng ta đang xây What we're building អ្វីដែលយើងកំពុងសង់
Bookmarks created with AI-generated summaries.
Vì sao quan trọng Why this matters ហេតុអ្វីសំខាន់
Seeing the summary appear in the response confirms the AI integration is working end-to-end.
bash
npx wrangler dev --remote

Tạo bookmark và xem summary Create a bookmark and check the summary បង្កើត bookmark ហើយមើល summary

bash
curl -X POST http://localhost:8787/bookmarks \
  -H "Content-Type: application/json" \
  -d '{"url":"https://developers.cloudflare.com/workers-ai/","title":"Workers AI Documentation","tags":"docs,ai"}' | jq

The response should include a summary field with an AI-generated description.

The response should include a summary field with an AI-generated description.

The response should include a summary field with an AI-generated description.

Kiểm tra summary đã lưu trong D1 Verify summaries are stored in D1 ពិនិត្យថា summary បានរក្សានៅ D1

bash
# List bookmarks - each should have a summary
curl -s http://localhost:8787/bookmarks | jq '.bookmarks[].summary'