Tutorial Mirascope: Toolkit Pythonic untuk Membangun Aplikasi LLM
Pendahuluan
Mirascope adalah library Python yang menyediakan antarmuka bersih dan Pythonic untuk berinteraksi dengan Large Language Models (LLM). Berbeda dengan framework besar seperti LangChain yang memiliki banyak abstraksi berlapis, Mirascope mengambil pendekatan minimalis dengan memanfaatkan fitur-fitur bawaan Python seperti decorator, type hints, dan Pydantic models.
Keunggulan utama Mirascope terletak pada kemampuannya menyederhanakan prompt engineering, structured output extraction, dan tool calling tanpa mengorbankan fleksibilitas. Library ini mendukung berbagai provider LLM termasuk OpenAI, Anthropic, Google Gemini, Mistral, Groq, dan Cohere dalam satu API yang konsisten.
Dalam tutorial ini, kita akan mempelajari cara menggunakan Mirascope mulai dari instalasi, prompt engineering dasar, structured output, tool calling, hingga teknik-teknik lanjutan seperti chaining, streaming, dan response model validation.
Mengapa Mirascope?
Sebelum masuk ke implementasi, berikut alasan mengapa Mirascope layak dipertimbangkan:
Instalasi
Instalasi Dasar
Mirascope bisa diinstal menggunakan pip dengan provider yang diinginkan:
# Instalasi core mirascope
pip install mirascope
Instalasi dengan provider spesifik
pip install "mirascope[openai]"
pip install "mirascope[anthropic]"
pip install "mirascope[gemini]"
pip install "mirascope[groq]"
Instalasi dengan semua provider
pip install "mirascope[all]"
Setup Environment Variables
Konfigurasi API key untuk provider yang akan digunakan:
# OpenAI
export OPENAIAPIKEY="sk-your-openai-key"
Anthropic
export ANTHROPICAPIKEY="sk-ant-your-anthropic-key"
Google Gemini
export GOOGLEAPIKEY="your-google-key"
Groq
export GROQAPIKEY="your-groq-key"
Atau menggunakan file .env dengan python-dotenv:
pip install python-dotenv
from dotenv import loaddotenv
load
dotenv()
Verifikasi Instalasi
import mirascope
print(f"Mirascope version: {mirascope.version}")
Basic Usage: Prompt Engineering
Membuat Prompt Sederhana dengan Decorator
Mirascope menggunakan decorator @prompttemplate dan fungsi call untuk menghasilkan respons dari LLM:
from mirascope.core import openai, prompttemplate
@openai.call("gpt-4o-mini")
@prompttemplate("Jelaskan tentang {topic} dalam 3 paragraf")
def explain(topic: str): ...
response = explain("machine learning")
print(response.content)
Kode di atas sangat ringkas. Decorator @openai.call menentukan provider dan model, sementara @prompttemplate mendefinisikan template prompt. Parameter topic secara otomatis diisi ke dalam template.
Multi-Provider Support
Salah satu keunggulan Mirascope adalah kemudahan berpindah antar provider:
from mirascope.core import openai, anthropic, gemini, prompttemplate
@openai.call("gpt-4o-mini")
@prompt
template("Apa itu {concept}?")
def askopenai(concept: str): ...
@anthropic.call("claude-sonnet-4-20250514")
@prompttemplate("Apa itu {concept}?")
def askanthropic(concept: str): ...
@gemini.call("gemini-1.5-flash")
@prompttemplate("Apa itu {concept}?")
def askgemini(concept: str): ...
Semua fungsi memiliki interface yang sama
for askfn in [askopenai, askanthropic, askgemini]:
response = askfn("neural network")
print(f"[{askfn.name}]: {response.content[:100]}...")
System Prompt dan Messages
Untuk skenario yang lebih kompleks, kita bisa menggunakan messages:
from mirascope.core import openai, Messages
@openai.call("gpt-4o-mini")
def translate(text: str, targetlang: str) -> Messages.Type:
return [
Messages.System(
"Kamu adalah penerjemah profesional. "
"Terjemahkan teks yang diberikan dengan akurat dan natural."
),
Messages.User(
f"Terjemahkan ke bahasa {targetlang}: {text}"
),
]
result = translate("Hello, how are you?", "Indonesia")
print(result.content)
Dynamic Prompt dengan Computed Fields
from mirascope.core import openai, prompttemplate
@openai.call("gpt-4o-mini")
@prompttemplate(
"""
SYSTEM: Kamu adalah asisten data analyst yang ahli.
USER:
Analisis dataset berikut dan berikan insight utama:
Kolom: {columns}
Jumlah baris: {rowcount}
Sample data:
{sampledata}
"""
)
def analyzedataset(
columns: list[str],
rowcount: int,
sampledata: str,
): ...
response = analyzedataset(
columns=["nama", "usia", "gaji", "departemen"],
rowcount=1500,
sampledata="Ahmad, 28, 8500000, Engineering\nSiti, 32, 12000000, Management",
)
print(response.content)
Structured Output: Ekstraksi Data Terstruktur
Fitur paling powerful dari Mirascope adalah kemampuan mengekstrak data terstruktur menggunakan Pydantic models.
Basic Extraction
from pydantic import BaseModel, Field
from mirascope.core import openai, prompttemplate
class BookInfo(BaseModel):
title: str = Field(description="Judul buku")
author: str = Field(description="Nama penulis")
year: int = Field(description="Tahun terbit")
genre: str = Field(description="Genre buku")
summary: str = Field(description="Ringkasan singkat dalam 1-2 kalimat")
@openai.call("gpt-4o-mini", responsemodel=BookInfo)
@prompttemplate("Ekstrak informasi dari review buku berikut: {review}")
def extractbookinfo(review: str): ...
review = """
Laskar Pelangi karya Andrea Hirata yang terbit tahun 2005 adalah novel
inspiratif yang menceritakan perjuangan anak-anak di Belitung untuk
mendapatkan pendidikan. Novel fiksi ini telah menjadi bestseller nasional.
"""
book = extractbookinfo(review)
print(f"Judul: {book.title}")
print(f"Penulis: {book.author}")
print(f"Tahun: {book.year}")
print(f"Genre: {book.genre}")
print(f"Ringkasan: {book.summary}")
Nested Models dan Lists
from pydantic import BaseModel, Field
from mirascope.core import openai, prompttemplate
class Ingredient(BaseModel):
name: str = Field(description="Nama bahan")
amount: str = Field(description="Jumlah/takaran")
unit: str = Field(description="Satuan (gram, ml, sdm, dll)")
class RecipeStep(BaseModel):
stepnumber: int = Field(description="Nomor langkah")
instruction: str = Field(description="Instruksi memasak")
durationminutes: int | None = Field(
default=None, description="Durasi dalam menit jika ada"
)
class Recipe(BaseModel):
name: str = Field(description="Nama masakan")
servings: int = Field(description="Jumlah porsi")
preptimeminutes: int = Field(description="Waktu persiapan dalam menit")
cooktimeminutes: int = Field(description="Waktu memasak dalam menit")
ingredients: list[Ingredient] = Field(description="Daftar bahan")
steps: list[RecipeStep] = Field(description="Langkah-langkah memasak")
tips: list[str] = Field(description="Tips memasak")
@openai.call("gpt-4o", responsemodel=Recipe)
@prompttemplate("Berikan resep lengkap untuk: {dishname}")
def getrecipe(dishname: str): ...
recipe = getrecipe("Nasi Goreng Spesial")
print(f"Resep: {recipe.name}")
print(f"Porsi: {recipe.servings}")
print(f"Waktu masak: {recipe.cooktimeminutes} menit")
print(f"\nBahan ({len(recipe.ingredients)}):")
for ing in recipe.ingredients:
print(f" - {ing.name}: {ing.amount} {ing.unit}")
print(f"\nLangkah ({len(recipe.steps)}):")
for step in recipe.steps:
print(f" {step.stepnumber}. {step.instruction}")
Validation dengan Pydantic
Karena Mirascope menggunakan Pydantic, kita mendapatkan validasi data secara gratis:
from pydantic import BaseModel, Field, fieldvalidator
from mirascope.core import openai, prompt
template
class SentimentResult(BaseModel):
text: str = Field(description="Teks yang dianalisis")
sentiment: str = Field(description="positif, negatif, atau netral")
confidence: float = Field(
description="Skor confidence antara 0 dan 1", ge=0, le=1
)
keyphrases: list[str] = Field(description="Frasa kunci yang menentukan sentimen")
@fieldvalidator("sentiment")
@classmethod
def validatesentiment(cls, v: str) -> str:
allowed = {"positif", "negatif", "netral"}
if v.lower() not in allowed:
raise ValueError(f"Sentiment harus salah satu dari: {allowed}")
return v.lower()
@openai.call("gpt-4o-mini", responsemodel=SentimentResult)
@prompttemplate("Analisis sentimen dari teks berikut: {text}")
def analyzesentiment(text: str): ...
result = analyzesentiment(
"Produk ini sangat bagus! Kualitasnya luar biasa dan pengiriman cepat."
)
print(f"Sentimen: {result.sentiment} (confidence: {result.confidence:.2%})")
print(f"Key phrases: {', '.join(result.keyphrases)}")
Tool Calling: Mengintegrasikan Fungsi Python
Mirascope memungkinkan LLM memanggil fungsi Python secara langsung, memungkinkan integrasi dengan API eksternal, database, atau logika bisnis.
Mendefinisikan Tools
from mirascope.core import openai, prompttemplate, BaseTool
from pydantic import Field
import json
class GetWeather(BaseTool):
"""Mendapatkan informasi cuaca untuk sebuah kota."""
city: str = Field(description="Nama kota")
country: str = Field(default="ID", description="Kode negara (ISO 3166)")
def call(self) -> str:
# Simulasi API cuaca
weather
data = {
"Jakarta": {"temp": 32, "condition": "Berawan", "humidity": 78},
"Bandung": {"temp": 24, "condition": "Cerah", "humidity": 65},
"Surabaya": {"temp": 33, "condition": "Panas", "humidity": 72},
}
data = weatherdata.get(
self.city, {"temp": 28, "condition": "Tidak diketahui", "humidity": 70}
)
return json.dumps(
{"city": self.city, "country": self.country, *data}, ensureascii=False
)
class CalculateDistance(BaseTool):
"""Menghitung jarak antara dua kota."""
cityfrom: str = Field(description="Kota asal")
cityto: str = Field(description="Kota tujuan")
def call(self) -> str:
distances = {
("Jakarta", "Bandung"): 150,
("Jakarta", "Surabaya"): 780,
("Bandung", "Surabaya"): 680,
}
key = (self.cityfrom, self.cityto)
reversekey = (self.cityto, self.cityfrom)
dist = distances.get(key, distances.get(reversekey, 0))
return f"Jarak {self.cityfrom} - {self.cityto}: {dist} km"
@openai.call("gpt-4o-mini", tools=[GetWeather, CalculateDistance])
@prompttemplate("{query}")
def travelassistant(query: str): ...
response = travelassistant(
"Bagaimana cuaca di Jakarta dan Bandung? "
"Berapa jarak antara kedua kota tersebut?"
)
if response.tools:
for tool in response.tools:
result = tool.call()
print(f"Tool: {tool.class.name}")
print(f"Result: {result}\n")
else:
print(response.content)
Tool Loop untuk Multi-Step Reasoning
from mirascope.core import openai, Messages, BaseTool
from pydantic import Field
class SearchDatabase(BaseTool):
"""Mencari data karyawan di database."""
query: str = Field(description="Kata kunci pencarian")
department: str | None = Field(
default=None, description="Filter berdasarkan departemen"
)
def call(self) -> str:
employees = [
{"nama": "Ahmad", "dept": "Engineering", "gaji": 15000000},
{"nama": "Siti", "dept": "Marketing", "gaji": 12000000},
{"nama": "Budi", "dept": "Engineering", "gaji": 18000000},
{"nama": "Dewi", "dept": "HR", "gaji": 10000000},
]
if self.department:
employees = [e for e in employees if e["dept"] == self.department]
return str(employees)
class CalculateAverage(BaseTool):
"""Menghitung rata-rata dari list angka."""
numbers: list[float] = Field(description="List angka untuk dihitung rata-rata")
def call(self) -> str:
avg = sum(self.numbers) / len(self.numbers)
return f"Rata-rata: {avg:,.0f}"
tools = [SearchDatabase, CalculateAverage]
def hrassistant(query: str) -> str:
messages = [
Messages.System(
"Kamu adalah HR assistant. Gunakan tools yang tersedia "
"untuk menjawab pertanyaan tentang data karyawan."
),
Messages.User(query),
]
@openai.call("gpt-4o-mini", tools=tools)
def call(messages: list) -> list:
return messages
response = call(messages)
while response.tools:
toolresults = []
for tool in response.tools:
result = tool.call()
toolresults.append(
Messages.Tool(toolcallid=tool.toolcall.id, content=result)
)
messages = [
messages,
response.messageparam,
toolresults,
]
response = call(messages)
return response.content
answer = hrassistant(
"Berapa rata-rata gaji di departemen Engineering?"
)
print(answer)
Advanced Usage
Streaming Response
Untuk respons yang panjang, streaming memberikan pengalaman yang lebih baik:
from mirascope.core import openai, prompttemplate
@openai.call("gpt-4o-mini", stream=True)
@prompttemplate("Tulis cerita pendek tentang {topic} dalam bahasa Indonesia")
def writestory(topic: str): ...
stream = writestory("robot yang belajar memasak")
for chunk, in stream:
print(chunk.content, end="", flush=True)
print()
Mengakses metadata setelah streaming selesai
print(f"\nTokens used: {stream.inputtokens} input, {stream.outputtokens} output")
Streaming Structured Output
from pydantic import BaseModel, Field
from mirascope.core import openai, prompttemplate
class ArticleOutline(BaseModel):
title: str = Field(description="Judul artikel")
sections: list[str] = Field(description="Daftar section/bagian artikel")
targetaudience: str = Field(description="Target pembaca")
estimatedwordcount: int = Field(description="Estimasi jumlah kata")
@openai.call("gpt-4o-mini", responsemodel=ArticleOutline, stream=True)
@prompttemplate("Buat outline artikel tentang: {topic}")
def createoutline(topic: str): ...
stream = createoutline("Implementasi CI/CD untuk Machine Learning")
for partialoutline in stream:
if partialoutline.title:
print(f"Title: {partialoutline.title}")
if partialoutline.sections:
print(f"Sections so far: {len(partialoutline.sections)}")
finaloutline = stream.constructedresponsemodel
print(f"\nFinal outline: {finaloutline.modeldumpjson(indent=2)}")
Chaining Calls
Mirascope memudahkan chaining multiple LLM calls:
from pydantic import BaseModel, Field
from mirascope.core import openai, prompttemplate
class TopicAnalysis(BaseModel):
mainthemes: list[str] = Field(description="Tema utama")
complexity: str = Field(description="Tingkat kompleksitas: dasar/menengah/lanjut")
prerequisites: list[str] = Field(description="Prasyarat yang dibutuhkan")
class TutorialPlan(BaseModel):
title: str = Field(description="Judul tutorial")
sections: list[str] = Field(description="Daftar section")
codeexamplesneeded: int = Field(description="Jumlah code example yang dibutuhkan")
estimatedduration: str = Field(description="Estimasi durasi belajar")
@openai.call("gpt-4o-mini", responsemodel=TopicAnalysis)
@prompttemplate("Analisis topik berikut untuk pembuatan tutorial: {topic}")
def analyzetopic(topic: str): ...
@openai.call("gpt-4o-mini", responsemodel=TutorialPlan)
@prompttemplate(
"""
Buat rencana tutorial berdasarkan analisis berikut:
Topik: {topic}
Tema utama: {themes}
Kompleksitas: {complexity}
Prasyarat: {prerequisites}
"""
)
def plantutorial(
topic: str, themes: str, complexity: str, prerequisites: str
): ...
Chain the calls
topic = "FastAPI untuk Machine Learning Deployment"
analysis = analyzetopic(topic)
plan = plantutorial(
topic=topic,
themes=", ".join(analysis.mainthemes),
complexity=analysis.complexity,
prerequisites=", ".join(analysis.prerequisites),
)
print(f"Tutorial: {plan.title}")
print(f"Durasi: {plan.estimatedduration}")
print(f"Jumlah sections: {len(plan.sections)}")
print(f"Code examples: {plan.codeexamplesneeded}")
for i, section in enumerate(plan.sections, 1):
print(f" {i}. {section}")
Call Parameters dan Configuration
from mirascope.core import openai, prompttemplate
@openai.call(
"gpt-4o-mini",
call
params={
"temperature": 0.7,
"maxtokens": 1000,
"topp": 0.9,
},
)
@prompttemplate("Berikan {count} ide kreatif untuk: {topic}")
def brainstorm(topic: str, count: int = 5): ...
response = brainstorm("aplikasi AI untuk UMKM Indonesia", count=5)
print(response.content)
Error Handling dan Retry
from tenacity import retry, stopafterattempt, waitexponential
from mirascope.core import openai, prompttemplate
@retry(
stop=stopafterattempt(3),
wait=waitexponential(multiplier=1, min=2, max=10),
)
@openai.call("gpt-4o-mini")
@prompttemplate("Ringkas teks berikut dalam 2 kalimat: {text}")
def summarizewithretry(text: str): ...
try:
result = summarizewithretry("Teks panjang yang akan diringkas...")
print(result.content)
except Exception as e:
print(f"Gagal setelah 3 percobaan: {e}")
Async Support
Mirascope mendukung async calls untuk performa yang lebih baik:
import asyncio
from mirascope.core import openai, prompttemplate
@openai.call("gpt-4o-mini")
@prompttemplate("Berikan fakta menarik tentang: {topic}")
async def getfact(topic: str): ...
async def main():
topics = ["Python", "Machine Learning", "Indonesia", "Kopi", "Batik"]
tasks = [getfact(topic) for topic in topics]
results = await asyncio.gather(tasks)
for topic, result in zip(topics, results):
print(f"\n{topic}: {result.content[:150]}...")
asyncio.run(main())
Custom Response Model dengan Enum
from enum import Enum
from pydantic import BaseModel, Field
from mirascope.core import openai, prompttemplate
class Priority(str, Enum):
LOW = "rendah"
MEDIUM = "sedang"
HIGH = "tinggi"
CRITICAL = "kritis"
class Category(str, Enum):
BUG = "bug"
FEATURE = "feature"
IMPROVEMENT = "improvement"
DOCUMENTATION = "documentation"
class TicketClassification(BaseModel):
category: Category = Field(description="Kategori tiket")
priority: Priority = Field(description="Tingkat prioritas")
affectedcomponent: str = Field(description="Komponen yang terpengaruh")
suggestedassigneeteam: str = Field(description="Tim yang disarankan")
estimatedefforthours: float = Field(description="Estimasi effort dalam jam")
summary: str = Field(description="Ringkasan tiket dalam 1 kalimat")
@openai.call("gpt-4o-mini", responsemodel=TicketClassification)
@prompttemplate(
"Klasifikasi tiket support berikut dan tentukan prioritas "
"serta tim yang harus menangani:\n\n{ticketdescription}"
)
def classifyticket(ticketdescription: str): ...
ticket = """
Login page menampilkan error 500 sejak deployment terakhir kemarin malam.
Semua user tidak bisa login ke production. Sudah dicoba clear cache tapi
masih error. Error log menunjukkan NullPointerException di AuthService.
"""
classification = classifyticket(ticket)
print(f"Kategori: {classification.category.value}")
print(f"Prioritas: {classification.priority.value}")
print(f"Komponen: {classification.affectedcomponent}")
print(f"Tim: {classification.suggestedassigneeteam}")
print(f"Estimasi: {classification.estimatedefforthours} jam")
print(f"Ringkasan: {classification.summary}")
Best Practices
1. Gunakan Type Hints Secara Konsisten
from pydantic import BaseModel, Field
from mirascope.core import openai, prompttemplate
class Output(BaseModel):
result: str = Field(description="Deskripsi yang jelas membantu LLM")
confidence: float = Field(ge=0, le=1, description="Skor antara 0-1")
@openai.call("gpt-4o-mini", responsemodel=Output)
@prompttemplate("{query}")
def process(query: str) -> None: ...
2. Pisahkan Prompt Template dari Logic
ANALYSISPROMPT = """
SYSTEM: Kamu adalah data analyst senior dengan keahlian di {domain}.
USER:
Analisis data berikut:
{data}
Fokus pada:
Trend utama
Anomali
Rekomendasi actionable
"""
@openai.call("gpt-4o", callparams={"temperature": 0.3})
@prompttemplate(ANALYSISPROMPT)
def analyze(domain: str, data: str): ...
3. Gunakan Response Models untuk Output yang Konsisten
Selalu gunakan Pydantic models ketika output perlu diproses lebih lanjut oleh kode. Ini memastikan:
- Validasi otomatis
- Type safety
- Dokumentasi implicit melalui field descriptions
- Retry otomatis jika output tidak sesuai schema
4. Manfaatkan Async untuk Batch Processing
import asyncio
from mirascope.core import openai, prompttemplate
@openai.call("gpt-4o-mini")
@prompttemplate("Terjemahkan ke bahasa Inggris: {text}")
async def translate(text: str): ...
async def batchtranslate(texts: list[str]) -> list[str]:
tasks = [translate(text) for text in texts]
results = await asyncio.gather(tasks)
return [r.content for r in results]
5. Implementasi Logging dan Monitoring
import time
from mirascope.core import openai, prompttemplate
def logcall(func):
def wrapper(args, *kwargs):
start = time.time()
result = func(args, kwargs)
duration = time.time() - start
print(
f"[{func.name}] "
f"Model: {result.model} | "
f"Tokens: {result.inputtokens}+{result.outputtokens} | "
f"Duration: {duration:.2f}s"
)
return result
return wrapper
@logcall
@openai.call("gpt-4o-mini")
@prompttemplate("Jawab pertanyaan: {question}")
def answer(question: str): ...
6. Organisasi Project yang Baik
Struktur project yang direkomendasikan untuk aplikasi berbasis Mirascope:
myproject/
├── prompts/
│ ├── analysis.py # Prompt templates untuk analisis
│ ├── extraction.py # Prompt templates untuk ekstraksi
│ └── generation.py # Prompt templates untuk generasi
├── models/
│ ├── schemas.py # Pydantic response models
│ └── tools.py # Tool definitions
├── services/
│ ├── llm
service.py # LLM call functions
│ └── data_service.py # Data processing
├── config.py # Configuration
└── main.py # Entry point
Perbandingan dengan Framework Lain
| Fitur | Mirascope | LangChain | LlamaIndex |
|-------|-----------|-----------|------------|
| Learning Curve | Rendah | Tinggi | Sedang |
| Abstraksi | Minimal | Banyak | Sedang |
| Type Safety | Bawaan (Pydantic) | Partial | Partial |
| Ukuran Package | Kecil | Besar | Sedang |
| Provider Support | Multi | Multi | Multi |
| Fokus Utama | LLM Calls + Extraction | Chains + Agents | RAG + Indexing |
Mirascope cocok untuk developer yang menginginkan kontrol penuh atas interaksi LLM tanpa overhead framework yang besar. Jika Anda membutuhkan fitur RAG yang lengkap, LlamaIndex mungkin lebih sesuai. Untuk workflow orchestration yang kompleks, LangChain menawarkan lebih banyak built-in tools.
Kesimpulan
Mirascope menawarkan pendekatan yang segar untuk pengembangan aplikasi LLM di Python. Dengan desain yang Pythonic, dukungan multi-provider, dan type safety bawaan melalui Pydantic, library ini sangat cocok untuk:
- Prototyping cepat: Mulai dari prompt sederhana hingga structured output hanya dalam beberapa baris kode
- Production applications: Type safety dan validasi memastikan output yang konsisten
- Multi-provider setup: Mudah berpindah atau membandingkan antar LLM provider
- Data extraction pipeline**: Kombinasi Pydantic models dan LLM calls sangat powerful untuk ETL berbasis AI
Mulailah dengan use case sederhana seperti text extraction atau classification, kemudian secara bertahap gunakan fitur-fitur lanjutan seperti tool calling dan chaining sesuai kebutuhan. Kunci sukses menggunakan Mirascope adalah memanfaatkan kekuatan Python yang sudah Anda kuasai, bukan mempelajari abstraksi baru yang tidak perlu.
Dokumentasi lengkap dan contoh-contoh tambahan bisa ditemukan di repository resmi Mirascope. Komunitas yang aktif di GitHub juga siap membantu jika Anda menemui kendala dalam implementasi.