// Agent: gathers data from public sources, saves, builds summaries with AI. // ---- Public sources ---- const CITIES = [ { name: "Astana", lat: 51.16, lon: 71.47 }, { name: "Almaty", lat: 43.24, lon: 76.89 }, { name: "Shymkent", lat: 42.31, lon: 69.59 }, { name: "Pavlodar", lat: 52.3, lon: 76.93 } ]; async function getFx() { const r = await fetch("https://open.er-api.com/v6/latest/KZT", { signal: AbortSignal.timeout(15000) }); if (!r.ok) throw new Error("open.er-api.com " + r.status); const j = await r.json(); if (!j.rates) throw new Error("open.er-api.com: empty response"); // rate = KZT -> foreign; value = tenge per unit = 1/rate const out = {}; for (const c of ["USD", "EUR", "RUB", "CNY"]) { if (j.rates[c]) out[c] = +(1 / j.rates[c]).toFixed(c === "RUB" ? 3 : 1); } return out; } async function getWeather(c, days) { days = days || 1; const u = "https://api.open-meteo.com/v1/forecast?latitude=" + c.lat + "&longitude=" + c.lon + "&daily=temperature_2m_max,temperature_2m_min,precipitation_sum&timezone=Asia%2FAlmaty&past_days=" + (days - 1); const r = await fetch(u, { signal: AbortSignal.timeout(20000) }); if (!r.ok) throw new Error("open-meteo " + c.name + " " + r.status); const j = await r.json(); if (!j.daily) throw new Error("open-meteo: no data"); const d = j.daily; const out = []; for (let i = 0; i < d.time.length; i++) { if (d.temperature_2m_max[i] == null) continue; out.push({ date: d.time[i], city: c.name, max: +d.temperature_2m_max[i], min: +d.temperature_2m_min[i], rain: +d.precipitation_sum[i] }); } return out; } function today() { return new Date().toLocaleDateString("en-CA"); } async function collect() { const quotes = []; const weather = []; const sources = []; const errors = []; // Rates try { const map = await getFx(); const date = today(); for (const code of Object.keys(map).sort()) { quotes.push({ date, code, unit: "₸/юнит", value: map[code], prev: 0, changePct: 0, source: "open.er-api.com" }); } sources.push({ id: "fx", name: "Forex open.er-api.com", ok: true, count: quotes.length }); } catch (e) { errors.push("Rates: " + e.message); sources.push({ id: "fx", name: "Forex open.er-api.com", ok: false, error: e.message }); } // Weather for (const c of CITIES) { try { const w = await getWeather(c, 2); const last = w[w.length - 1]; if (last) weather.push(last); } catch (e) { errors.push("Weather " + c.name + ": " + e.message); } } sources.push({ id: "weather", name: "open-meteo.com (4 regions)", ok: weather.length === CITIES.length, count: weather.length }); // Agricultural commodity prices — planned after public access is opened sources.push({ id: "agrop", name: "Vetlex/ALAPI (agricultural prices)", ok: false, note: "Planned — container has no access to KZ sources" }); return { quotes, weather, sources, errors }; } // Наполнение истории погоды за N дней (курсы на сегодня берёт collect, // исторические курсов в ₸ нет — нет открытого источника, KZ API недоступны). async function backfill(nDays) { const n = nDays || 30; const weather = []; const errors = []; for (const c of CITIES) { try { const w = await getWeather(c, n); weather.push(...w.filter((x) => x.date <= today())); } catch (e) { errors.push("Weather history " + c.name + ": " + e.message); } } return { weather, forex: {}, errors }; } // ---- AI ---- async function ai(text) { const r = await fetch(process.env.AI_BASE_URL + "/chat/completions", { method: "POST", headers: { "Content-Type": "application/json", Authorization: "Bearer " + process.env.AI_API_KEY }, body: JSON.stringify({ model: process.env.AI_MODEL, messages: [ { role: "system", content: "You are an agricultural market price analysis assistant. Answer briefly in Russian, using only the data from the context. If the data is insufficient, state so." }, { role: "user", content: text } ] }) }); if (!r.ok) throw new Error("AI is temporarily unavailable, code " + r.status); const j = await r.json(); const out = j.choices && j.choices[0] && j.choices[0].message && j.choices[0].message.content; if (!out) throw new Error("AI returned an empty response"); return out; } function fmt(d) { return d; } async function summarize(d) { const lastDate = d.quotes.length ? d.quotes[d.quotes.length - 1].date : "—"; const ctx = "Date " + lastDate + ":\n" + d.quotes.filter((q) => q.date === lastDate).map((q) => q.code + " = " + q.value + " tenge (" + (q.changePct >= 0 ? "+" : "") + q.changePct + "%)").join(", ") + "\nWeather: " + d.weather.filter((w) => w.date === lastDate).map((w) => w.city + " " + w.min + ".." + w.max + "°С, " + w.rain + " mm").join("; "); try { return await ai("Create a 3-5 sentence summary for the 'Price Agent' dashboard: what the data is about, what changed, possible impact on agricultural product prices and logistics.\nData:\n" + ctx); } catch (e) { return e.message; } } async function ask(ctx, q) { const text = "Question: " + q + "\nData:\n" + JSON.stringify(ctx, null, 1).slice(0, 6000); return await ai(text); } module.exports = { collect, backfill, summarize, ask };