Skip to content

10.4 Bot Telegram Personal

Topologi: A — Agent tunggal Materi yang dipakai: 3.2, 3.3Prasyarat: 10.3

Masalahnya

Bot WhatsApp dari kasus sebelumnya mengingat percakapan, tapi ingatannya bergulir. Dengan lastMessages: 10, apa pun yang disebut sebelas pesan lalu sudah hilang.

Untuk asisten HR itu cukup. Untuk asisten pribadi, tidak.

Bayangkan pengguna berkata di hari Senin: "panggil aku Rian, aku di Surabaya, jawab pakai bahasa santai ya." Hari Kamis ia bertanya sesuatu yang sama sekali berbeda. Ia berharap bot masih tahu namanya, kotanya, dan gaya bahasanya — padahal percakapan Senin sudah jauh di belakang.

Menaikkan lastMessages menjadi 200 bukan jawaban: kamu membayar 200 pesan di setiap panggilan hanya untuk mempertahankan empat fakta.

Keputusan arsitektur

Keputusan 1 — Fakta pengguna: message history atau working memory?

lastMessages besarWorking memory
Yang disimpanSeluruh percakapanRingkasan fakta terpilih
Ukuran per panggilanTumbuh terusTetap kecil
Bertahan lintas threadTidakYa (scope resource)
Bisa dibaca manusiaSulitYa — blok Markdown rapi
Risiko fakta hilangTinggi saat percakapan panjangRendah

Pilihan: working memory dengan scope: 'resource'.

Ini persis masalah yang dirancang untuk diselesaikannya, seperti dibahas di 3.2: memisahkan fakta yang bertahan dari percakapan yang bergulir.

Keputusan 2 — Lingkup resource atau thread?

scope: 'resource'scope: 'thread'
Ingatan bertahanLintas semua percakapan penggunaHanya di satu percakapan
Cocok untukAsisten pribadiSesi konsultasi, perangkat bersama
Risiko bila resource salahProfil bocor antar penggunaTerbatas satu thread

Pilihan: resource.

Telegram memberi chat.id yang stabil per pengguna, jadi identitasnya jelas. Tapi perhatikan kolom terakhir — konsekuensi salah di sini lebih besar daripada di kasus 10.3.

Working memory memperbesar akibat resource yang salah

Di 10.3, resource yang tertukar berarti riwayat percakapan tercampur. Di sini, dengan working memory berlingkup resource, yang tercampur adalah profil pribadi — nama, kota, preferensi.

Pastikan resource berasal dari chat.id yang dikirim Telegram, bukan dari nama yang diketik pengguna dalam pesan.

Keputusan 3 — Perlu semantic recall juga?

Cukup working memory+ Semantic recall
Komponen tambahanNolVector store + embedder
Biaya per giliranNol tambahan2 panggilan embedding
LatensiTidak berubah+1 langkah jaringan
Bisa temukan percakapan lamaTidakYa, berdasarkan makna

Pilihan: mulai tanpa semantic recall.

Working memory menyelesaikan 80% kebutuhan asisten pribadi dengan biaya nol tambahan. Semantic recall baru dibutuhkan saat pengguna merujuk percakapan lama secara spesifik — "waktu itu kita pernah bahas lensa apa ya?"

Cara menambahkannya nanti ditunjukkan di bagian akhir kasus ini.

Struktur direktori

text
asisten-telegram/
├── src/
│   ├── infra/
│   │   └── telegram.ts          ← kirim & baca pesan Telegram
│   ├── server/
│   │   └── webhook.ts           ← pola sama seperti 10.3
│   └── mastra/
│       ├── agents/
│       │   └── tg-agent.ts      ← agent + working memory
│       └── index.ts
├── .env
└── package.json

Strukturnya identik dengan kasus 10.3 — hanya infra/ yang berganti isi. Itu bukti pemisahan lapisan di kasus sebelumnya bekerja.

Alur

Kuncinya ada di panah A->>WM pada sesi kedua: profil dimuat meski thread-nya berbeda.

Implementasi

src/mastra/agents/tg-agent.ts

ts
import { Agent } from '@mastra/core/agent'
import { Memory } from '@mastra/memory'

export const tgAgent = new Agent({
  id: 'tg-agent',
  name: 'Asisten Pribadi',
  instructions: `Kamu asisten pribadi di Telegram.

Perhatikan preferensi pengguna dan sesuaikan jawabanmu: bahasa, panjang
jawaban, dan hal-hal yang ia minati.

Jawab ringkas kecuali diminta rinci.`,
  model: 'openai/gpt-5-mini',
  memory: new Memory({
    options: {
      lastMessages: 5,
      workingMemory: {
        enabled: true,
        scope: 'resource',
        template: `# Profil Pengguna
- **Nama**:
- **Kota**:
- **Preferensi bahasa**:
- **Minat**:
`,
      },
    },
  }),
})

Tiga hal yang menentukan kualitas di berkas ini.

lastMessages justru diturunkan ke 5. Karena fakta penting sekarang disimpan di working memory, riwayat pesan tidak perlu panjang. Ini menghemat biaya, bukan menambah.

Template hanya 4 field. Godaannya menulis 15 field "untuk jaga-jaga". Jangan — working memory ikut dikirim di setiap panggilan, dan field yang tidak pernah mempengaruhi jawaban adalah biaya murni.

Nama field tidak ambigu. Preferensi bahasa lebih baik daripada Bahasa, yang bisa diartikan "bahasa yang dikuasai" atau "bahasa yang diinginkan".

src/infra/telegram.ts

ts
const API = `https://api.telegram.org/bot${process.env.TG_TOKEN}`

export async function kirimPesan(chatId: number, teks: string): Promise<void> {
  const res = await fetch(`${API}/sendMessage`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ chat_id: chatId, text: teks }),
  })
  if (!res.ok) console.error('gagal kirim TG', res.status, await res.text())
}

export function bacaPesanMasuk(body: unknown): { chatId: number; teks: string } | null {
  const m = (body as any)?.message
  if (!m?.text) return null
  return { chatId: m.chat.id, teks: m.text }
}

Bentuknya sengaja dibuat sama persis dengan infra/whatsapp.ts: kirimPesan dan bacaPesanMasuk. Antarmuka yang seragam membuat server/webhook.ts hampir tidak berubah antar kanal.

src/server/webhook.ts

ts
import { mastra } from '../mastra/index.ts'
import { kirimPesan, bacaPesanMasuk } from '../infra/telegram.ts'

async function prosesPesan(chatId: number, teks: string): Promise<void> {
  const agent = mastra.getAgentById('tg-agent')

  const hasil = await agent.generate(teks, {
    memory: {
      resource: `tg:${chatId}`,        // profil — bertahan lintas sesi
      thread: `tg:${chatId}:${hariIni()}`,  // percakapan — direset harian
    },
    maxSteps: 5,
  })

  await kirimPesan(chatId, hasil.text)
}

function hariIni(): string {
  return new Date().toISOString().slice(0, 10)   // 2026-09-06
}

export async function handleWebhook(body: unknown): Promise<void> {
  const pesan = bacaPesanMasuk(body)
  if (!pesan) return

  void prosesPesan(pesan.chatId, pesan.teks).catch(err => {
    console.error('gagal memproses pesan', err)
  })
}

Perbedaan halus dari kasus 10.3

Perhatikan thread di sini memuat tanggal, sementara resource tidak.

Akibatnya: percakapan direset tiap hari — pertanyaan kemarin tidak mengotori konteks hari ini — tapi profil tetap bertahan karena ia berlingkup resource.

Ini pola yang berguna untuk asisten pribadi, dan berbeda dari WhatsApp di kasus 10.3 yang memang cocok satu utas abadi.

Hasil nyata

Dijalankan sungguhan. Sesi kedua memakai thread yang benar-benar baru:

text
===== sesi-1 =====
> Halo, aku Rian dari Surabaya. Tolong selalu jawab pakai bahasa Indonesia
  santai ya.
< Halo Rian! Sip—aku bakal selalu jawab pakai Bahasa Indonesia santai.
  Ada yang mau ditanyain atau dibantu sekarang?

> aku lagi belajar fotografi malam
< Keren, Rian — mantap! Aku catat kamu lagi belajar fotografi malam.

===== sesi-2 (THREAD BARU, resource sama) =====
> rekomendasiin kegiatan akhir pekan dong
< Oke Rian—nih beberapa ide akhir pekan di Surabaya yang pas buat kamu yang
  suka fotografi malam. Santai aja, pilih sesuai mood:

1) Sesi foto malam di Jembatan Suramadu (sisi Surabaya)
   - Kenapa: lampu jembatan + garis panjang cocok buat long exposure.
   - Tips: bawa tripod, lensa wide + tele, remote shutter, datang 30–60
     menit sebelum blue hour.

2) Surabaya North Quay / area pelabuhan
   ...

Pesan di sesi-2 hanya berbunyi "rekomendasiin kegiatan akhir pekan dong" — tidak menyebut nama, kota, minat, atau gaya bahasa. Tapi jawabannya memuat keempatnya:

Yang muncul di jawabanBerasal dari field
"Oke Rian"Nama
"di Surabaya"Kota
"yang suka fotografi malam"Minat
"nih", "santai aja", "buat kamu"Preferensi bahasa

Empat field template terisi di sesi-1 dan terbawa utuh ke sesi-2. Itu yang tidak bisa dilakukan message history, berapa pun besar lastMessages-nya — karena thread-nya memang berbeda.

Menambahkan semantic recall

Saat pengguna mulai merujuk percakapan lama secara spesifik, tambahkan komponennya. Perubahannya terbatas di konstruktor Memory:

ts
import { Memory } from '@mastra/memory'
import { LibSQLStore, LibSQLVector } from '@mastra/libsql'
import { ModelRouterEmbeddingModel } from '@mastra/core/llm'

memory: new Memory({
  storage: new LibSQLStore({ id: 'tg-storage', url: 'file:./tg.db' }),
  vector: new LibSQLVector({ id: 'tg-vector', url: 'file:./tg.db' }),
  embedder: new ModelRouterEmbeddingModel('openai/text-embedding-3-small'),
  options: {
    lastMessages: 5,
    semanticRecall: true,
    workingMemory: { enabled: true, scope: 'resource', template: '...' },
  },
})
UntungRugi
Bisa menemukan percakapan lama berdasarkan makna2 panggilan embedding tiap giliran
Tidak perlu pengguna mengingat kata persisnyaLatensi bertambah satu langkah jaringan
Bekerja berdampingan dengan working memoryMengganti model embedding jadi migrasi data

Perhatikan storage dan vector menunjuk berkas yang sama — untuk pengembangan lokal itu praktis, seperti dibahas di 3.3.

Best practice yang dipakai di sini

1. Antarmuka infra/ yang seragam antar kanal. kirimPesan dan bacaPesanMasuk punya bentuk sama di WhatsApp dan Telegram, sehingga lapisan di atasnya tidak peduli kanal mana.

2. Template working memory sependek mungkin. Empat field yang benar-benar mempengaruhi jawaban, bukan lima belas yang "mungkin berguna".

3. thread dan resource boleh punya aturan berbeda. Thread bertanggal

  • resource stabil adalah kombinasi yang memberi percakapan bersih tiap hari tanpa kehilangan profil.

4. Turunkan lastMessages saat working memory aktif. Keduanya menyelesaikan masalah berbeda; membiarkan lastMessages besar setelah memasang working memory berarti membayar dua kali untuk hal yang sama.

Kapan pola ini salah

SituasiKenapa gagalKe mana
Perangkat dipakai bergantian banyak orangProfil orang sebelumnya terbawaPakai scope: 'thread'
Sesi berlangsung berhari-hari tanpa putusWorking memory tidak merangkum jalannya pekerjaan3.4 Observational memory
Pengguna bertanya isi dokumenIni pengetahuan, bukan profil10.5 Helpdesk
Bot harus proaktif mengirim pesanPola ini hanya bereaksi10.6 Terjadwal

Coba sendiri

Bangun asisten belajar bahasa di Telegram yang menyesuaikan diri dengan tiap murid.

Template working memory harus memuat: nama, tingkat kemampuan, topik yang sudah dikuasai, dan pola kesalahan yang berulang.

Selesai kalau

  • [ ] Setelah 4 giliran di sesi-1, working memory di Studio memuat keempat field terisi
  • [ ] Di thread baru, agent menyapa dengan nama dan menyesuaikan tingkat kesulitan
  • [ ] Field "pola kesalahan" terisi hasil pengamatan agent, bukan karena murid menyebutkannya
  • [ ] Dua resource berbeda tidak saling melihat profil
  • [ ] lastMessages tidak lebih dari 6

Kalau macet

Field "pola kesalahan" selalu kosong? Nama field-nya kurang tegas. Model tidak tahu ia diharapkan menyimpulkan. Coba: Pola kesalahan yang kamu amati (isi sendiri berdasarkan jawaban murid, jangan tunggu diberitahu).

Profil tidak terbawa ke thread baru? Periksa scope: 'resource' — kalau thread atau tidak diisi, ingatannya terkurung di satu percakapan.

Lanjut ke mana

Bot ini mengenal penggunanya, tapi pengetahuannya masih terbatas apa yang ada di instruksi. Kasus berikutnya memberinya akses ke ratusan halaman dokumen — 10.5 Helpdesk Dokumen.

Materi belajar mandiri. Bukan dokumentasi resmi Mastra.