````javascript // TATO-OS // Learning AI V1.3 // Route: /api/learning-ai const MODEL = "@cf/zai-org/glm-4.7-flash"; const HEADERS = { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-store" }; function json(data, status = 200) { return new Response(JSON.stringify(data), { status, headers: HEADERS }); } function id() { return crypto.randomUUID(); } function n(value) { const x = Number(value); return Number.isFinite(x) ? x : 0; } function s(value) { return value == null ? "" : String(value); } function extractText(response) { if (!response) return null; if (typeof response === "string") { return response.trim() || null; } if (typeof response.response === "string") { return response.response.trim() || null; } if ( response.result && typeof response.result.response === "string" ) { return response.result.response.trim() || null; } if (typeof response.content === "string") { return response.content.trim() || null; } if (Array.isArray(response.content)) { const value = response.content .map(item => { if (typeof item === "string") return item; if (item && typeof item.text === "string") { return item.text; } if (item && typeof item.content === "string") { return item.content; } return ""; }) .join("\n") .trim(); if (value) return value; } const message = response?.choices?.[0]?.message; if (message) { if (typeof message.content === "string") { return message.content.trim() || null; } if (Array.isArray(message.content)) { const value = message.content .map(item => { if (typeof item === "string") return item; if (item && typeof item.text === "string") { return item.text; } return ""; }) .join("\n") .trim(); if (value) return value; } } if (typeof response.output_text === "string") { return response.output_text.trim() || null; } return null; } function parseJSON(value) { if (!value) return null; let text = value .replace(/^```json\s*/i, "") .replace(/^```\s*/i, "") .replace(/\s*```$/i, "") .trim(); try { const parsed = JSON.parse(text); if (parsed && typeof parsed === "object") { return parsed; } } catch (_) {} const start = text.indexOf("{"); const end = text.lastIndexOf("}"); if (start >= 0 && end > start) { try { const parsed = JSON.parse( text.slice(start, end + 1) ); if (parsed && typeof parsed === "object") { return parsed; } } catch (_) {} } return null; } function fallback(metrics, learning, content) { const traffic = n(metrics.product_views) > 0 || n(metrics.clicks) > 0 || n(metrics.engagements) > 0; const customer = n(metrics.customers) > 0; const order = n(metrics.orders) > 0; let action = "DISTRIBUTE"; let priority = "LOW"; let metric = "Product Views"; if (order) { action = "SCALE"; priority = "HIGH"; metric = "Orders"; } else if (customer) { action = "OPTIMIZE"; priority = "MEDIUM"; metric = "Customers"; } else if (traffic) { action = "OPTIMIZE"; priority = "MEDIUM"; metric = "Engagements"; } return { summary: traffic ? "พบพฤติกรรมจาก Content แล้ว แต่ยังต้องเก็บข้อมูลเพิ่ม" : "ยังไม่มีข้อมูลพฤติกรรมจาก Content จึงยังประเมินประสิทธิภาพไม่ได้", observed_signals: [ `Learning signal คือ ${s( learning.signal_type || "NO_TRAFFIC" )}`, `Product Views ${n(metrics.product_views)}`, `Orders ${n(metrics.orders)}` ], learning: { what_we_learned: order ? "Content มีหลักฐานเชื่อมโยงกับ Conversion" : traffic ? "Content เริ่มสร้างพฤติกรรม แต่ยังไม่มีหลักฐาน Conversion เพียงพอ" : "ยังไม่มี behavioral evidence เพียงพอสำหรับตัดสิน Content", confidence: order ? "HIGH" : traffic ? "MEDIUM" : "LOW" }, problems: order ? [] : traffic ? ["ยังไม่มี Conversion เพียงพอ"] : [ "ยังไม่มี Traffic", "ยังไม่มีข้อมูลสำหรับวัด Conversion" ], next_content: { action, direction: traffic ? "ปรับ Content จากพฤติกรรมที่เกิดขึ้นเพื่อเพิ่ม Conversion" : "เผยแพร่ Content ปัจจุบันเพื่อสร้าง Traffic", angle: content?.angle || "เชื่อมความสนใจของลูกค้ากับความต้องการเรื่องกาแฟ", cta: content?.cta || "ดูรายละเอียดและทดลอง TATO", success_metric: metric }, next_action: { type: action, reason: order ? "พบ Conversion แล้ว" : traffic ? "มีพฤติกรรมแล้ว ควร Optimize" : "ยังไม่มี Traffic ควร Distribute" }, priority }; } async function ensureTables(db) { await db.prepare(` CREATE TABLE IF NOT EXISTS ai_runs ( id TEXT PRIMARY KEY, customer_id TEXT, run_type TEXT, model TEXT, input_data TEXT, output_data TEXT, status TEXT, tokens_used INTEGER, created_at TEXT ) `).run(); await db.prepare(` CREATE TABLE IF NOT EXISTS ai_insights ( id TEXT PRIMARY KEY, customer_id TEXT, run_id TEXT, insight_type TEXT, title TEXT, content TEXT, score REAL, priority TEXT, status TEXT, created_at TEXT ) `).run(); } async function getLearning(db) { try { const row = await db.prepare(` SELECT * FROM learning_feedback ORDER BY created_at DESC LIMIT 1 `).first(); if (row) return row; } catch (_) {} try { const row = await db.prepare(` SELECT * FROM content_measurements ORDER BY measured_at DESC LIMIT 1 `).first(); if (row) { return { id: row.id, signal_type: row.status === "WAITING_FOR_TRAFFIC" ? "NO_TRAFFIC" : "MEASUREMENT", title: row.status === "WAITING_FOR_TRAFFIC" ? "ยังไม่มี Traffic" : "Content Measurement", finding: row.status === "WAITING_FOR_TRAFFIC" ? "ยังไม่พบกิจกรรมที่ใช้เรียนรู้จาก Content" : "มีข้อมูลจาก Content Measurement", score: row.status === "WAITING_FOR_TRAFFIC" ? 20 : 50, measurement_start: row.measurement_start }; } } catch (_) {} return { id: null, signal_type: "NO_TRAFFIC", title: "ยังไม่มี Traffic", finding: "ยังไม่พบกิจกรรมที่ใช้เรียนรู้จาก Content", score: 20 }; } async function getContent(db) { try { return await db.prepare(` SELECT * FROM content_engine ORDER BY created_at DESC LIMIT 1 `).first(); } catch (_) { return null; } } async function getMetrics(db, learning) { const start = learning?.measurement_start || learning?.created_at || "1970-01-01T00:00:00.000Z"; const metrics = { attention: 0, product_views: 0, clicks: 0, engagements: 0, customers: 0, orders: 0, revenue: 0 }; try { const rows = await db.prepare(` SELECT event_type, COUNT(*) AS total FROM behavior_events WHERE created_at >= ? GROUP BY event_type `) .bind(start) .all(); for (const row of rows.results || []) { const type = s(row.event_type).toLowerCase(); const total = n(row.total); if ( type === "product_view" || type === "view" || type === "page_view" ) { metrics.product_views += total; metrics.attention += total; } if (type.includes("click")) { metrics.clicks += total; } if ( type.includes("engagement") || type.includes("like") || type.includes("comment") || type.includes("share") ) { metrics.engagements += total; } } } catch (_) {} try { const row = await db.prepare(` SELECT COUNT(*) AS total FROM customers WHERE created_at >= ? `) .bind(start) .first(); metrics.customers = n(row?.total); } catch (_) {} try { const row = await db.prepare(` SELECT COUNT(*) AS orders, COALESCE(SUM(amount), 0) AS revenue FROM orders WHERE created_at >= ? `) .bind(start) .first(); metrics.orders = n(row?.orders); metrics.revenue = n(row?.revenue); } catch (_) {} return metrics; } function conversions(m) { const rate = (a, b) => b > 0 ? Number(((a / b) * 100).toFixed(2)) : 0; return { attention_to_view: rate(n(m.product_views), n(m.attention)), view_to_click: rate(n(m.clicks), n(m.product_views)), click_to_customer: rate(n(m.customers), n(m.clicks)), customer_to_order: rate(n(m.orders), n(m.customers)), engagement_to_order: rate(n(m.orders), n(m.engagements)) }; } function prompt(learning, content, metrics, conversion) { return ` วิเคราะห์ข้อมูล Learning ของ TATO Coffee ตอบเป็น JSON เท่านั้น ห้ามอธิบายเหตุผล ห้ามใช้ Markdown Learning: ${JSON.stringify(learning)} Content: ${JSON.stringify({ id: content?.id || null, title: content?.title || null, status: content?.status || null, objective: content?.objective || null, attention_type: content?.attention_type || null, market_keyword: content?.market_keyword || null, angle: content?.angle || null, cta: content?.cta || null })} Metrics: ${JSON.stringify(metrics)} Conversion: ${JSON.stringify(conversion)} ต้องคืนโครงสร้างนี้: { "summary": "สรุปสั้นภาษาไทย", "observed_signals": ["สัญญาณ"], "learning": { "what_we_learned": "สิ่งที่เรียนรู้", "confidence": "LOW" }, "problems": ["ปัญหา"], "next_content": { "action": "DISTRIBUTE", "direction": "ทิศทาง Content", "angle": "มุม Content", "cta": "CTA", "success_metric": "Product Views" }, "next_action": { "type": "DISTRIBUTE", "reason": "เหตุผล" }, "priority": "LOW" } Allowed action: DISTRIBUTE, OPTIMIZE, SCALE, WAIT Allowed priority: LOW, MEDIUM, HIGH `; } async function analyze(env) { if (!env.DB) { throw new Error("D1 binding DB is missing"); } if (!env.AI) { throw new Error("Workers AI binding AI is missing"); } await ensureTables(env.DB); const learning = await getLearning(env.DB); const content = await getContent(env.DB); const metrics = await getMetrics( env.DB, learning ); const conversion = conversions(metrics); let aiStatus = "FALLBACK_ANALYZED"; let aiText = null; let analysis = null; let aiError = null; try { const response = await env.AI.run( MODEL, { messages: [ { role: "system", content: "Return ONLY valid JSON. No reasoning. No markdown." }, { role: "user", content: prompt( learning, content, metrics, conversion ) } ], reasoning_effort: "low", max_completion_tokens: 1200, temperature: 0.1, response_format: { type: "json_object" } } ); aiText = extractText(response); analysis = parseJSON(aiText); if (analysis) { aiStatus = "AI_ANALYZED"; } } catch (error) { aiError = error?.message || String(error); } if (!analysis) { analysis = fallback( metrics, learning, content ); } return { learning, content, metrics, conversion, ai: { status: aiStatus, model: MODEL, analysis, debug: { ai_called: aiText !== null, response_text_received: !!aiText, parsed_json: aiStatus === "AI_ANALYZED", error: aiError } } }; } async function save(env, result) { const runId = id(); const insightId = id(); const now = new Date().toISOString(); const input = JSON.stringify({ learning: result.learning, content: result.content, metrics: result.metrics, conversion: result.conversion }); const output = JSON.stringify(result.ai.analysis); await env.DB.prepare(` INSERT INTO ai_runs ( id, customer_id, run_type, model, input_data, output_data, status, tokens_used, created_at ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) `) .bind( runId, null, "LEARNING_ANALYSIS", MODEL, input, output, result.ai.status, null, now ) .run(); const priority = s( result.ai.analysis?.priority ).toUpperCase() || "LOW"; const score = priority === "HIGH" ? 90 : priority === "MEDIUM" ? 60 : 30; await env.DB.prepare(` INSERT INTO ai_insights ( id, customer_id, run_id, insight_type, title, content, score, priority, status, created_at ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) `) .bind( insightId, null, "LEARNING", "LEARNING", "Learning AI Analysis", output, score, priority, "NEW", now ) .run(); result.ai.run_id = runId; result.ai.insight_id = insightId; return result; } export async function onRequestGet(context) { try { const result = await analyze(context.env); return json({ success: true, layer: "LEARNING_AI_V1", mode: "preview", status: "ANALYZED", learning: result.learning, content: result.content ? { id: result.content.id, title: result.content.title, status: result.content.status } : null, metrics: result.metrics, conversion: result.conversion, ai: result.ai, next_step: result.ai.status === "AI_ANALYZED" ? "AI Learning analysis ready. Run POST execute to save." : "AI fallback analysis returned." }); } catch (error) { return json( { success: false, layer: "LEARNING_AI_V1", error: error?.message || String(error) }, 500 ); } } export async function onRequestPost(context) { try { let body = {}; try { body = await context.request.json(); } catch (_) {} const mode = body?.mode || "preview"; const result = await analyze(context.env); if (mode === "execute") { const saved = await save( context.env, result ); return json({ success: true, layer: "LEARNING_AI_V1", mode: "execute", status: "EXECUTED", learning: saved.learning, content: saved.content ? { id: saved.content.id, title: saved.content.title, status: saved.content.status } : null, metrics: saved.metrics, conversion: saved.conversion, ai: saved.ai, next_step: "Learning AI saved. Next stage: AI → Action Engine." }); } return json({ success: true, layer: "LEARNING_AI_V1", mode: "preview", status: "ANALYZED", learning: result.learning, content: result.content ? { id: result.content.id, title: result.content.title, status: result.content.status } : null, metrics: result.metrics, conversion: result.conversion, ai: result.ai, next_step: "AI Learning analysis ready." }); } catch (error) { return json( { success: false, layer: "LEARNING_AI_V1", error: error?.message || String(error) }, 500 ); } } ````