Yaliyomo

Wiki 14: Generative AI na LLMs — Kutumia Claude API kwa Python

Mwezi 4 | Lengo kuu: Mwanafunzi aelewe LLM ni nini, atumie Claude API kwa usalama (API key kwenye .env), ajenge chatbot ya Kiswahili kwenye terminal, kisha aiweke AI ndani ya endpoint ya FastAPI.


Maandalizi ya Mwalimu (soma kabla ya wiki kuanza)

  • MUHIMU — fanya mapema, kabla ya Kipindi 1:
    1. Fungua akaunti kwenye Claude Console: https://platform.claude.com (zamani console.anthropic.com).
    2. Weka salio (credits) dogo — mf. USD 5 inatosha kwa wiki nzima ya mazoezi kwa model ya bei nafuu.
    3. Weka kikomo cha matumizi (spend limit) kwenye Console → Settings → Limits, ili mwanafunzi asitumie pesa nyingi kwa bahati mbaya.
    4. Tengeneza API key (Console → API Keys → Create Key). Ipe jina kama mwanafunzi-wiki14. Nakili mara moja — haionyeshwi tena.
    5. Mpe mwanafunzi key hiyo kwa njia ya faragha (si kwenye Google Meet chat inayorekodiwa, si kwenye screen share). Mwisho wa kozi unaweza kuifuta (revoke).
  • Soma:
  • Sakinisha:

    powershell
    mkdir wiki14_ai
    cd wiki14_ai
    python -m venv venv
    .\venv\Scripts\Activate.ps1
    pip install anthropic python-dotenv fastapi "uvicorn[standard]" pytest httpx
    
  • Models tutakazotumia: | Model ID | Matumizi | Bei (USD kwa tokens milioni 1: input / output) | |----------|----------|------| | claude-haiku-5-5 | Mazoezi ya darasani — haraka na nafuu sana | $0.10 / $0.50 | | claude-sonnet-5-5 | Ubora wa juu zaidi (kazi ngumu) | $2 / $10 |

    Ushauri: darasani tumia claude-haiku-5-5 karibu muda wote. Tutaonyesha claude-sonnet-5-5 mara moja tu kulinganisha. Bei zinaweza kubadilika — angalia ukurasa wa pricing kabla ya darasa.

  • Usionyeshe API key yako kwenye screen share! Kabla ya kushare screen, funga faili .env. Ukiionyesha kwa bahati mbaya: nenda Console, ifute (revoke), tengeneza mpya.


Malengo ya Wiki (mwisho wa wiki mwanafunzi ataweza...)

  1. Kueleza LLM (Large Language Model) ni nini kwa lugha rahisi, na token ni nini.
  2. Kuandika prompt nzuri (wazi, yenye muktadha, mfano wa jibu).
  3. Kuhifadhi API key kwenye .env, kuisoma kwa python-dotenv, na kuhakikisha .env haiingii Git.
  4. Kuita Claude API kwa anthropic SDK (client.messages.create) na kusoma jibu, stop_reason na usage.
  5. Kuhesabu gharama ya ombi (request) kutokana na tokens.
  6. Kutumia system prompt na historia ya mazungumzo (multi-turn).
  7. Kutumia streaming ili jibu lionekane neno kwa neno.
  8. Kuelewa dhana ya tool use na structured JSON output.
  9. Kujenga chatbot ya Kiswahili kwenye terminal, na endpoint ya FastAPI POST /uliza.
  10. Kujua mbadala wa bure (Ollama — models za ndani ya kompyuta).

Kipindi 1 — LLM ni nini? API key na ombi la kwanza

Mtiririko wa kipindi

  1. Marudio ya kazi ya nyumbani ya Wiki 13 (alama za mtihani)
  2. LLM ni nini? Tokens, prompts, tofauti na ML ya Wiki 13
  3. API keys, .env, .gitignore — usalama kwanza
  4. Live coding: ombi la kwanza, kusoma jibu, usage na gharama
  5. Makosa (errors) na jinsi ya kuyashika
  6. Zoezi darasani
  7. Q&A + kazi ya nyumbani

Elezo kwa mwalimu (soma kwanza wewe)

Wiki 13 vs Wiki 14. Wiki iliyopita tulifundisha models wenyewe (ndogo, kwa kazi moja, mf. bei ya kiwanja). Wiki hii tunatumia model kubwa sana iliyofundishwa tayari na kampuni (Anthropic) kwa maandishi mengi mno ya dunia. Hatuifundishi — tunaiita kupitia internet (API) na kulipia kwa matumizi.

LLM (Large Language Model) ni model ya ML iliyofundishwa kutabiri kipande kinachofuata cha maandishi. Kwa kufanya hivyo kwa kiwango kikubwa sana, imejifunza lugha, ukweli mwingi, na hata kufikiri hatua kwa hatua. "Generative AI" = AI inayozalisha (generate) maudhui mapya: maandishi, code, picha.

Token. LLM haisomi herufi wala maneno kamili — inasoma vipande vinavyoitwa tokens. Kwa Kiingereza, token moja ≈ robo tatu ya neno. Kwa Kiswahili, mara nyingi neno moja linagawanywa vipande zaidi (mf. "tunawapenda" → vipande kadhaa), kwa hiyo maandishi ya Kiswahili mara nyingi yana tokens zaidi kuliko yale yale kwa Kiingereza. Hii ni muhimu kwa sababu tunalipa kwa tokens — za kuingiza (input) na za kutoka (output). Output ni ghali zaidi.

Prompt = maelekezo/swali tunalompa model. Prompt nzuri: (1) wazi na mahususi, (2) ina muktadha (nani anauliza, kwa nini), (3) inaeleza muundo wa jibu (mf. "orodha ya vitu 3", "sentensi 2"), (4) inaweza kuwa na mfano.

API key ni kama nenosiri + kadi ya benki kwa pamoja: yeyote mwenye key yako anaweza kutumia salio lako. Kwa hiyo:

  • Haiandikwi ndani ya code (api_key="sk-ant-..." — HAPANA).
  • Inawekwa kwenye faili .env, ambalo linaongezwa kwenye .gitignore kabla ya commit ya kwanza.
  • python-dotenv inasoma .env na kuweka thamani kwenye "environment variables". SDK ya anthropic inatafuta yenyewe variable inayoitwa ANTHROPIC_API_KEY.

Muundo wa ombi (request):

python
client.messages.create(
    model="claude-haiku-5-5",   # model ipi
    max_tokens=1024,            # kikomo cha juu cha tokens za jibu
    messages=[{"role": "user", "content": "..."}],  # mazungumzo
)

Jibu (response) ni object yenye:

  • response.content — list ya "blocks". Block ya maandishi ina type == "text" na .text. Models za kisasa zinaweza pia kurudisha block ya "thinking" (mawazo ya ndani) kabla ya maandishi, kwa hiyo tunachuja blocks za "text" badala ya kudhani content[0] ni maandishi.
  • response.stop_reason — kwa nini ilisimama: "end_turn" (imemaliza kawaida), "max_tokens" (imekatwa — ongeza max_tokens), "tool_use" (inataka kutumia tool — Kipindi 3), "refusal" (imekataa kujibu kwa sababu za usalama).
  • response.usage.input_tokens, response.usage.output_tokens — kwa kuhesabu gharama.

Effort (kiasi cha "kufikiri"). Models hizi zinafikiri kidogo kabla ya kujibu, na tokens za kufikiri zinahesabiwa kama output (zinalipiwa). Kwa mazungumzo rahisi tunaweza kupunguza kwa output_config={"effort": "low"} — jibu la haraka na nafuu zaidi. Kwa kazi ngumu, acha default.

Sema hivi kwa mwanafunzi

"Wiki iliyopita tulimfundisha 'dalali' wetu mdogo kutabiri bei za viwanja. Wiki hii tunamtumia 'profesa' aliyesoma maktaba nzima ya dunia — hatumfundishi, tunamuuliza tu na kulipa kwa kila neno analoandika."

"LLM ni kama mchezo wa kumalizia sentensi. Nikisema 'Mama anapika ugali na...' unasema 'mboga' au 'samaki'. LLM imefanya mchezo huo mabilioni ya mara, ikawa hodari sana."

"Token ni kama vipande vya muwa — model haili muwa mzima, inakata vipande. Tunalipa kwa kila kipande."

"API key ni kama PIN ya M-Pesa. Hutamwandikia mtu PIN yako kwenye ubao wa matangazo. GitHub ya umma ni ubao wa matangazo! Kuna roboti zinazotafuta keys zilizovuja kila dakika."

Andika code hii live (share screen)

Kabla ya kushare screen: andaa .env yako mapema, na usiifungue ukiwa unashare.

Hatua 1: .gitignore KWANZA

powershell
git init

Tengeneza faili .gitignore:

text
venv/
.env
__pycache__/

Hatua 2: .env na .env.example Faili .env (lisilo la kushare):

text
ANTHROPIC_API_KEY=sk-ant-weka-key-yako-hapa

Faili .env.example (hili linaweza ku-commitiwa — linaonyesha wengine variables zinazohitajika bila siri):

text
ANTHROPIC_API_KEY=

Thibitisha:

powershell
git status

.env haipaswi kuonekana kwenye orodha. Ikionekana, .gitignore ina kosa.

Hatua 3: Ombi la kwanza — kwanza.py

python
# kwanza.py
from dotenv import load_dotenv
import anthropic

load_dotenv()                     # inasoma .env -> ANTHROPIC_API_KEY
client = anthropic.Anthropic()    # inachukua key yenyewe kutoka mazingira

response = client.messages.create(
    model="claude-haiku-5-5",
    max_tokens=1024,
    messages=[
        {"role": "user", "content": "Habari! Nieleze Python ni nini kwa sentensi mbili, kwa Kiswahili."}
    ],
)

# response.content ni list ya blocks; tunachukua zile za maandishi tu
jibu = "".join(block.text for block in response.content if block.type == "text")
print(jibu)
print("-" * 40)
print("stop_reason:", response.stop_reason)
print("Tokens - input:", response.usage.input_tokens, "| output:", response.usage.output_tokens)
powershell
python kwanza.py

Matokeo (maandishi yatatofautiana kila mara — hilo ni la kawaida):

text
Python ni lugha ya programu iliyo rahisi kusoma na kuandika, inayotumika kutengeneza
tovuti, uchambuzi wa data na akili bandia. ...
----------------------------------------
stop_reason: end_turn
Tokens - input: 28 | output: 74

Sema: "Endesha tena — jibu litabadilika kidogo. LLM si kama function ya kawaida inayorudisha kitu kile kile kila mara."

Hatua 4: Function ya kusaidia + gharama — msaidizi.py

python
# msaidizi.py
from dotenv import load_dotenv
import anthropic

load_dotenv()
client = anthropic.Anthropic()

# Bei kwa USD kwa tokens MILIONI 1 (angalia ukurasa wa pricing, zinaweza kubadilika)
BEI = {
    "claude-haiku-5-5": {"input": 0.10, "output": 0.50},
    "claude-sonnet-5-5": {"input": 2.00, "output": 10.00},
}
USD_KWA_TSH = 2600   # takriban — badilisha kulingana na kiwango cha siku


def maandishi(response) -> str:
    """Rudisha maandishi yote kutoka kwenye jibu."""
    return "".join(b.text for b in response.content if b.type == "text")


def gharama(response, model: str) -> float:
    """Rudisha gharama ya ombi kwa USD."""
    b = BEI[model]
    return (response.usage.input_tokens * b["input"]
            + response.usage.output_tokens * b["output"]) / 1_000_000


def uliza(swali: str, model: str = "claude-haiku-5-5") -> str:
    response = client.messages.create(
        model=model,
        max_tokens=1024,
        messages=[{"role": "user", "content": swali}],
    )
    usd = gharama(response, model)
    print(f"[{model}] tokens in={response.usage.input_tokens} out={response.usage.output_tokens} "
          f"| gharama ≈ ${usd:.6f} (≈ TSh {usd * USD_KWA_TSH:.2f})")
    return maandishi(response)


if __name__ == "__main__":
    print(uliza("Nipe methali tatu za Kiswahili kuhusu bidii, na maana zake kwa ufupi."))

Matokeo (takriban):

text
[claude-haiku-5-5] tokens in=30 out=180 | gharama ≈ $0.000093 (≈ TSh 0.24)
1. Haba na haba hujaza kibaba — ...

Sema: "Chini ya shilingi moja! Lakini ukiwa na watumiaji 10,000 kwa siku, gharama inaonekana. Ndiyo maana tunapima."

Hatua 5: Kuhesabu tokens kabla ya kutuma (Kiswahili vs Kiingereza)

python
from msaidizi import client

for sentensi in ["Tunawapenda sana wanafunzi wetu wote.",
                 "We love all our students very much."]:
    hesabu = client.messages.count_tokens(
        model="claude-haiku-5-5",
        messages=[{"role": "user", "content": sentensi}],
    )
    print(f"{hesabu.input_tokens:3d} tokens  <-  {sentensi}")

Matokeo yataonyesha Kiswahili kina tokens zaidi kidogo kwa maana ile ile. (Namba kamili inategemea model.) count_tokens ni bure.

Hatua 6: Kupunguza "kufikiri" kwa maswali rahisi

python
response = client.messages.create(
    model="claude-haiku-5-5",
    max_tokens=1024,
    output_config={"effort": "low"},   # jibu la haraka na nafuu
    messages=[{"role": "user", "content": "2 + 2 ni ngapi? Jibu kwa namba tu."}],
)
print(maandishi(response), response.usage.output_tokens)

Hatua 7: Kushika makosa (errors)

python
import anthropic
from msaidizi import client, maandishi

try:
    r = client.messages.create(
        model="claude-haiku-5-5",
        max_tokens=200,
        messages=[{"role": "user", "content": "Sema habari"}],
    )
    print(maandishi(r))
except anthropic.AuthenticationError:
    print("API key si sahihi au haipo. Angalia faili .env")
except anthropic.RateLimitError:
    print("Maombi mengi mno kwa muda mfupi. Subiri kidogo ujaribu tena.")
except anthropic.APIConnectionError:
    print("Hakuna mtandao au server haipatikani.")
except anthropic.APIStatusError as e:
    print(f"Hitilafu ya API ({e.status_code}): {e.message}")

Onyesha kosa la key kwa makusudi: badilisha .env kuwa key ya uongo (bila kushare .env!), endesha → "API key si sahihi...". Rudisha key sahihi.
SDK inajaribu tena (retry) yenyewe mara 2 kwa makosa ya muda (429, 5xx).

Maswali ya kumuuliza mwanafunzi

  1. Swali: Tofauti kubwa kati ya model ya Wiki 13 na LLM ni ipi? Jibu: Wiki 13 tulifundisha model ndogo wenyewe kwa kazi moja; LLM imefundishwa tayari kwa data kubwa sana, tunaiita kupitia API na inaweza kazi nyingi za lugha.

  2. Swali: Kwa nini tusiandike API key ndani ya kwanza.py? Jibu: Tukicommit/kushare code, key inavuja na mtu anaweza kutumia salio letu. Tunaiweka .env iliyo kwenye .gitignore.

  3. Swali: stop_reason == "max_tokens" ina maana gani na tufanye nini? Jibu: Jibu limekatwa kabla ya kumaliza; ongeza max_tokens au omba jibu fupi zaidi.

  4. Swali: Kwa nini tusitumie response.content[0].text moja kwa moja? Jibu: content ni list ambayo inaweza kuwa na blocks za aina nyingine (mf. thinking) mwanzoni; tunachuja type == "text".

  5. Swali: Ni ipi ghali zaidi: input tokens au output tokens? Jibu: Output (mara 5 zaidi kwa models hizi).

Makosa ya kawaida

Kosa Ujumbe Suluhisho
Hakuna .env / jina baya la variable TypeError: Could not resolve authentication method... (inatokea unapotuma ombi, si unapotengeneza client) Hakikisha .env iko folda ile ile unayoendesha, jina ni ANTHROPIC_API_KEY, na load_dotenv() imeitwa kabla ya Anthropic()
Key si sahihi / imefutwa anthropic.AuthenticationError: Error code: 401 ... invalid x-api-key Tengeneza key mpya Console
Salio limekwisha anthropic.BadRequestError: Error code: 400 ... credit balance is too low Ongeza credits kwenye Console → Billing
Jina la model limekosewa anthropic.NotFoundError: Error code: 404 ... model: claude-haiku Tumia ID kamili: claude-haiku-5-5
.env imecommitiwa (hakuna error — hatari kimya kimya!) Futa key Console MARA MOJA, tengeneza mpya, git rm --cached .env, ongeza .env kwenye .gitignore
Kuweka nafasi/alama za nukuu .env Key inasomwa vibaya → 401 Andika ANTHROPIC_API_KEY=sk-ant-... bila nafasi kuzunguka =
Kusahau max_tokens TypeError: ... missing required argument 'max_tokens' max_tokens ni lazima

Zoezi darasani

Zoezi 1.1: Andika script tafsiri.py inayomwomba Claude atafsiri sentensi 3 za Kiingereza kwenda Kiswahili. Chapisha tafsiri na gharama.

Zoezi 1.2: Uliza swali lile lile kwa max_tokens=20. Chapisha stop_reason. Kisha kwa max_tokens=1024. Eleza tofauti.

Zoezi 1.3: Andika prompt mbili za kumwomba Claude aeleze "variable" kwenye Python: (a) prompt fupi isiyo wazi: "variable?", (b) prompt nzuri yenye muktadha na muundo. Linganisha majibu.

Jibu
python
# tafsiri.py
from msaidizi import client, maandishi, gharama

MODEL = "claude-haiku-5-5"

# 1.1
sentensi = """1. The market opens at seven in the morning.
2. Please send me the report by Friday.
3. Learning to code takes patience."""
r = client.messages.create(
    model=MODEL,
    max_tokens=500,
    messages=[{"role": "user", "content":
        f"Tafsiri sentensi hizi kwenda Kiswahili sanifu. Rudisha tafsiri tu, zikiwa na namba zile zile:\n{sentensi}"}],
)
print(maandishi(r))
print(f"Gharama: ${gharama(r, MODEL):.6f}")

# 1.2
for kikomo in [20, 1024]:
    r = client.messages.create(
        model=MODEL,
        max_tokens=kikomo,
        messages=[{"role": "user", "content": "Eleza historia ya Tanzania kwa aya mbili."}],
    )
    print(f"max_tokens={kikomo}: stop_reason={r.stop_reason}")
# 20 -> "max_tokens" (jibu limekatwa, au hata halina maandishi kama tokens ziliisha wakati wa kufikiri)
# 1024 -> "end_turn" (limekamilika)

# 1.3
mbaya = "variable?"
nzuri = ("Mimi ni mwanafunzi ninayeanza kujifunza Python. Nieleze 'variable' ni nini "
         "kwa Kiswahili rahisi, kwa sentensi 3, kisha nipe mfano mmoja mfupi wa code.")
for p in [mbaya, nzuri]:
    r = client.messages.create(model=MODEL, max_tokens=600,
                               messages=[{"role": "user", "content": p}])
    print(f"=== PROMPT: {p}\n{maandishi(r)}\n")
# Prompt nzuri inatoa jibu la Kiswahili, fupi, lenye mfano — kwa sababu tuliomba hivyo.

Kipindi 2 — System prompt, historia ya mazungumzo na Streaming

Mtiririko wa kipindi

  1. Marudio ya kazi ya nyumbani (Kipindi 1)
  2. System prompt — "kumpa model kazi/utambulisho"
  3. Multi-turn: model haikumbuki — sisi tunatuma historia
  4. Chatbot ya Kiswahili kwenye terminal
  5. Streaming — jibu neno kwa neno
  6. Zoezi darasani
  7. Q&A + kazi ya nyumbani

Elezo kwa mwalimu (soma kwanza wewe)

System prompt (system="...") ni maelekezo ya jumla yanayotumika kwa mazungumzo yote: utambulisho ("Wewe ni msaidizi wa duka la Mama Neema"), lugha ("jibu kwa Kiswahili"), mtindo ("fupi, la kirafiki"), mipaka ("usijibu maswali yasiyohusu duka"). Inatofautiana na ujumbe wa user — ni kama maelekezo ya meneja kwa mfanyakazi kabla ya kufungua duka.

Model HAINA kumbukumbu. Kila ombi la API linasimama peke yake. Ili model "ikumbuke" mazungumzo, sisi tunatuma historia yote kila mara:

python
messages = [
    {"role": "user", "content": "Jina langu ni Juma."},
    {"role": "assistant", "content": "Karibu Juma!"},
    {"role": "user", "content": "Jina langu ni nani?"},   # sasa itajibu "Juma"
]

Sheria: ujumbe unapishana user → assistant → user ..., na wa kwanza ni user. Matokeo ya gharama: kila zamu tunatuma historia yote tena, kwa hiyo input tokens zinaongezeka kila ujumbe. Mazungumzo marefu = ghali zaidi. Ndiyo maana chatbots huwa na amri ya "anza upya" (reset) au hupunguza historia.

Kuhifadhi jibu la assistant kwenye historia: Njia salama zaidi ni kuweka response.content yote (list ya blocks) kama content ya assistant — SDK inaikubali, na inahifadhi blocks zote (pamoja na za thinking) kama API inavyopendekeza.

Streaming. Bila streaming, mtumiaji anasubiri sekunde kadhaa bila kuona kitu, kisha jibu lote linatokea. Kwa streaming, maneno yanatokea moja moja kama mtu anaandika — uzoefu bora zaidi, na majibu marefu hayakwami kwenye "timeout". SDK:

python
with client.messages.stream(model=..., max_tokens=..., messages=...) as stream:
    for text in stream.text_stream:
        print(text, end="", flush=True)
    final = stream.get_final_message()   # jibu kamili mwishoni (pamoja na usage)

flush=True inalazimisha maandishi yaonekane mara moja kwenye terminal.

Sema hivi kwa mwanafunzi

"System prompt ni kama sare na maelekezo unayompa mhudumu mpya wa mgahawa: 'Wewe unaitwa Neema, salimia wateja kwa Kiswahili, usizungumzie siasa.' Kisha wateja wanakuja na maswali yao."

"Model ni kama mtu mwenye usahaulifu kamili — kila unapomwita amesahau kila kitu. Kwa hiyo kila mara unampa daftari la mazungumzo yote ya awali asome kwanza. Daftari likiwa refu, anachukua muda zaidi kusoma — na unalipa zaidi."

"Streaming ni kama kuangalia mpira mubashara (live) badala ya kusubiri matokeo ya mwisho."

Andika code hii live (share screen)

Hatua 1: System prompt — system_demo.py

python
from msaidizi import client, maandishi

MODEL = "claude-haiku-5-5"
swali = "Nina shilingi elfu kumi. Nifanye nini nazo?"

for system in [
    "Wewe ni mshauri wa fedha mwenye tahadhari. Jibu kwa Kiswahili, kwa sentensi 3.",
    "Wewe ni mpishi maarufu wa Zanzibar. Kila jibu lihusishe chakula. Jibu kwa Kiswahili, sentensi 3.",
]:
    r = client.messages.create(
        model=MODEL,
        max_tokens=400,
        system=system,
        messages=[{"role": "user", "content": swali}],
    )
    print(f"SYSTEM: {system}\n-> {maandishi(r)}\n")

Sema: "Swali lile lile, majibu tofauti kabisa — kwa sababu tu ya system prompt."

Hatua 2: Model haikumbuki — kumbukumbu.py

python
from msaidizi import client, maandishi

MODEL = "claude-haiku-5-5"

# Ombi 1
r1 = client.messages.create(model=MODEL, max_tokens=200,
    messages=[{"role": "user", "content": "Jina langu ni Juma na ninaishi Arusha."}])
print("Bot:", maandishi(r1))

# Ombi 2 BILA historia
r2 = client.messages.create(model=MODEL, max_tokens=200,
    messages=[{"role": "user", "content": "Ninaishi wapi?"}])
print("Bila historia:", maandishi(r2))   # haijui!

# Ombi 2 PAMOJA na historia
r3 = client.messages.create(model=MODEL, max_tokens=200, messages=[
    {"role": "user", "content": "Jina langu ni Juma na ninaishi Arusha."},
    {"role": "assistant", "content": r1.content},
    {"role": "user", "content": "Ninaishi wapi?"},
])
print("Na historia:", maandishi(r3))     # "Arusha"

Hatua 3: Chatbot ya Kiswahili (CLI) na streaming — chatbot.py

python
# chatbot.py
from dotenv import load_dotenv
import anthropic

load_dotenv()
client = anthropic.Anthropic()

MODEL = "claude-haiku-5-5"
SYSTEM = (
    "Wewe ni 'Rafiki', msaidizi wa kirafiki anayezungumza Kiswahili sanifu cha Tanzania. "
    "Jibu kwa ufupi (sentensi 1-4) isipokuwa mtumiaji aombe maelezo marefu. "
    "Kama hujui jibu, sema hujui badala ya kubuni."
)


def main():
    historia = []
    jumla_in = jumla_out = 0
    print("Rafiki: Habari! Mimi ni Rafiki. Andika 'toka' kumaliza, au 'upya' kuanza upya.\n")

    while True:
        ujumbe = input("Wewe: ").strip()
        if not ujumbe:
            continue
        if ujumbe.lower() in {"toka", "exit", "quit"}:
            print(f"Rafiki: Kwaheri! (tokens jumla: in={jumla_in}, out={jumla_out})")
            break
        if ujumbe.lower() == "upya":
            historia = []
            print("Rafiki: Sawa, tumeanza upya.\n")
            continue

        historia.append({"role": "user", "content": ujumbe})
        try:
            print("Rafiki: ", end="", flush=True)
            with client.messages.stream(
                model=MODEL,
                max_tokens=2048,
                system=SYSTEM,
                output_config={"effort": "low"},
                messages=historia,
            ) as stream:
                for text in stream.text_stream:
                    print(text, end="", flush=True)
                final = stream.get_final_message()
            print("\n")
        except anthropic.APIError as e:
            print(f"\n[Hitilafu: {e}]\n")
            historia.pop()          # ondoa ujumbe ambao haukujibiwa
            continue

        historia.append({"role": "assistant", "content": final.content})
        jumla_in += final.usage.input_tokens
        jumla_out += final.usage.output_tokens


if __name__ == "__main__":
    main()
powershell
python chatbot.py

Mfano wa mazungumzo:

text
Rafiki: Habari! Mimi ni Rafiki. Andika 'toka' kumaliza, au 'upya' kuanza upya.

Wewe: Jina langu ni Asha
Rafiki: Karibu sana Asha! Nikusaidie nini leo?

Wewe: Nipe wazo la biashara ndogo kwa mtaji wa laki moja
Rafiki: Unaweza kuanza biashara ya kuuza juisi ya matunda ...

Wewe: Jina langu ni nani?
Rafiki: Jina lako ni Asha.

Wewe: toka
Rafiki: Kwaheri! (tokens jumla: in=412, out=168)

Onyesha: "Angalia in= — inaongezeka kila zamu kwa sababu tunatuma historia yote."

Maswali ya kumuuliza mwanafunzi

  1. Swali: Tofauti ya system na ujumbe wa user? Jibu: System ni maelekezo ya jumla ya tabia/utambulisho kwa mazungumzo yote; user ni ujumbe wa mtumiaji wa kila zamu.
  2. Swali: Kwa nini input tokens zinaongezeka kila zamu? Jibu: Tunatuma historia yote kila ombi, kwa sababu model haikumbuki.
  3. Swali: Faida mbili za streaming? Jibu: Mtumiaji anaona jibu mara moja (uzoefu bora); majibu marefu hayakwami kwa timeout.
  4. Swali: Amri ya upya inaokoa nini? Jibu: Inafuta historia, hivyo inapunguza tokens (gharama) na kuanza mada mpya.
  5. Swali: Kwa nini historia.pop() kwenye except? Jibu: Ili historia isibaki na ujumbe wa user usio na jibu (ujumbe mbili za user mfululizo).

Makosa ya kawaida

Kosa Ujumbe Suluhisho
Kuanza messages na assistant au role mbaya BadRequestError: 400 ... messages: ... Ujumbe wa kwanza uwe user; roles ni user/assistant tu (system inakaa kwenye system=)
Kuweka {"role": "system", ...} kama ujumbe wa kwanza BadRequestError: 400 Tumia parameter system="..."
Kusahau kuongeza jibu la assistant kwenye historia Bot "inasahau" historia.append({"role": "assistant", "content": final.content})
Maandishi ya streaming yanatoka mwishoni tu (hakuna error) Tumia print(text, end="", flush=True)
Kutumia stream.text_stream nje ya with RuntimeError/stream imefungwa Soma stream ndani ya block ya with
Herufi za Kiswahili/emoji zinaharibika kwenye terminal Alama za ajabu Tumia Windows Terminal; au $env:PYTHONIOENCODING="utf-8"

Zoezi darasani

Zoezi 2.1: Badilisha chatbot iwe "Mwalimu wa Hisabati" anayejibu kwa hatua kwa hatua, kwa Kiswahili, na kukataa kwa adabu maswali yasiyo ya hisabati.

Zoezi 2.2: Ongeza amri gharama inayochapisha gharama ya mazungumzo hadi sasa (USD na TSh).

Zoezi 2.3: Weka kikomo: historia isizidi ujumbe 10 (zamu 5). Ikizidi, futa zamu ya zamani zaidi (ujumbe 2 wa mwanzo).

Jibu
python
# chatbot_mwalimu.py
from dotenv import load_dotenv
import anthropic

load_dotenv()
client = anthropic.Anthropic()

MODEL = "claude-haiku-5-5"
BEI_IN, BEI_OUT = 0.10, 0.50       # USD kwa tokens milioni 1
USD_KWA_TSH = 2600
MAX_UJUMBE = 10

# 2.1
SYSTEM = (
    "Wewe ni Mwalimu wa Hisabati mvumilivu. Jibu kwa Kiswahili, ukionyesha hatua kwa hatua "
    "(Hatua 1, Hatua 2, ...). Kama swali halihusu hisabati, kataa kwa adabu na mwalike "
    "mwanafunzi aulize swali la hisabati."
)


def main():
    historia = []
    jumla_in = jumla_out = 0
    while True:
        ujumbe = input("Wewe: ").strip()
        if not ujumbe:
            continue
        if ujumbe.lower() == "toka":
            break
        # 2.2
        if ujumbe.lower() == "gharama":
            usd = (jumla_in * BEI_IN + jumla_out * BEI_OUT) / 1_000_000
            print(f"Gharama hadi sasa: ${usd:.6f} ≈ TSh {usd * USD_KWA_TSH:.2f}\n")
            continue

        historia.append({"role": "user", "content": ujumbe})
        print("Mwalimu: ", end="", flush=True)
        with client.messages.stream(model=MODEL, max_tokens=2048, system=SYSTEM,
                                    messages=historia) as stream:
            for text in stream.text_stream:
                print(text, end="", flush=True)
            final = stream.get_final_message()
        print("\n")
        historia.append({"role": "assistant", "content": final.content})
        jumla_in += final.usage.input_tokens
        jumla_out += final.usage.output_tokens

        # 2.3 — futa zamu ya zamani zaidi (user + assistant) historia ikizidi
        while len(historia) > MAX_UJUMBE:
            historia = historia[2:]


if __name__ == "__main__":
    main()

Kumbuka: tunafuta ujumbe 2 kwa pamoja ili historia ianze tena na user.


Kipindi 3 — Tool use, JSON output na AI endpoint kwenye FastAPI

Mtiririko wa kipindi

  1. Marudio ya kazi ya nyumbani (Kipindi 2)
  2. Structured JSON output — kupata data, si maneno tu
  3. Tool use — model "inaomba" kutumia function zetu
  4. FastAPI: POST /uliza, POST /uliza-stream, POST /chambua-maoni
  5. Kupima endpoint bila kutumia pesa (mock/monkeypatch)
  6. Gharama, usalama, na mbadala wa bure (Ollama)
  7. Zoezi darasani
  8. Q&A + kazi ya nyumbani ya wiki

Elezo kwa mwalimu (soma kwanza wewe)

Structured output (JSON). Mara nyingi programu yetu haitaki aya ya maneno — inataka data: {"hisia": "chanya", "alama": 4}. Tunampa API JSON schema (maelezo ya muundo) kupitia output_config={"format": {"type": "json_schema", "schema": {...}}}, na jibu linahakikishwa kuwa JSON inayolingana na schema. Kisha json.loads(...). Schema lazima iwe na "additionalProperties": False na "required".

Tool use (function calling). Model haiwezi kujua bei ya leo ya bidhaa kwenye duka letu, wala kuandika kwenye database yetu. Lakini tunaweza kumwambia: "Una tool inayoitwa pata_bei — ukihitaji bei, niombe." Mzunguko:

  1. Tunatuma swali + maelezo ya tools (jina, maelezo, input_schema).
  2. Model ikiamua inahitaji tool, inarudisha stop_reason == "tool_use" na block ya tool_use (jina + input, mf. {"bidhaa": "sukari"}).
  3. Sisi tunaendesha function yetu ya Python na kupata matokeo.
  4. Tunarudisha matokeo kama block ya tool_result (pamoja na tool_use_id ile ile) kwenye ujumbe wa user.
  5. Model inatumia matokeo kuandika jibu la mwisho.

    Model haiendeshi code yetu — inaomba tu; sisi ndio tunaamua na kuendesha. Hii ni muhimu kwa usalama.

AI ndani ya FastAPI. Sawa na Wiki 13: tunatengeneza client mara moja, endpoint inapokea Pydantic model, inaita Claude, inarudisha JSON. Kwa streaming tunatumia StreamingResponse ya FastAPI na generator inayotoa (yield) vipande vya maandishi.

  • Weka mipaka: max_length ya swali (kuzuia mtu kutuma maandishi makubwa mno = gharama), max_tokens ya jibu.
  • Badilisha errors za Anthropic kuwa HTTPException zenye status zinazofaa.
  • Usirudishe kamwe API key kwenye jibu au logs.

Kupima bila gharama: Kwenye pytest hatutaki kuita API halisi (pesa + jibu linabadilika). Tunatumia monkeypatch kubadilisha main.client kwa "client bandia" inayorudisha jibu lililoandaliwa.

Gharama na mbadala wa bure:

  • Chagua model nafuu (Haiku) kwa kazi rahisi; Sonnet kwa kazi ngumu tu.
  • Punguza max_tokens, historia, na urefu wa system prompt; effort: "low" kwa maswali rahisi.
  • Weka spend limit kwenye Console.
  • Ollama (https://ollama.com) inaendesha models za wazi (mf. Llama, Gemma, Qwen) kwenye kompyuta yako — bure, bila internet, lakini zinahitaji RAM/GPU nzuri, ni polepole zaidi, na kwa kawaida ubora (hasa Kiswahili) ni wa chini kuliko Claude.

Sema hivi kwa mwanafunzi

"Structured output ni kama kumpa mtu fomu ya kujaza badala ya kumwambia 'niandikie barua'. Fomu inakupa data iliyopangwa, rahisi kwa kompyuta kusoma."

"Tool use ni kama mhudumu wa benki anayekuambia 'ngoja nikaangalie salio lako kwenye mfumo'. Hajui salio kichwani — anatumia chombo (mfumo wa benki). Claude ni mhudumu; function zetu ni mfumo."

"Ukiweka AI kwenye API, kila mtu duniani anaweza kuitumia — na kila ombi linagharimu wewe pesa. Kwa hiyo tunaweka milango (mipaka) kama mlinzi wa getini."

Andika code hii live (share screen)

Hatua 1: Structured JSON — json_demo.py

python
import json
from msaidizi import client, maandishi

SCHEMA = {
    "type": "object",
    "properties": {
        "hisia": {"type": "string", "enum": ["chanya", "hasi", "wastani"]},
        "alama": {"type": "integer", "description": "1 (mbaya sana) hadi 5 (nzuri sana)"},
        "muhtasari": {"type": "string"},
    },
    "required": ["hisia", "alama", "muhtasari"],
    "additionalProperties": False,
}

maoni = "Chakula kilikuwa kitamu sana ila huduma ilichelewa karibu saa nzima."

r = client.messages.create(
    model="claude-haiku-5-5",
    max_tokens=1024,
    system="Unachambua maoni ya wateja wa mgahawa. Muhtasari uwe kwa Kiswahili, sentensi moja.",
    messages=[{"role": "user", "content": maoni}],
    output_config={"format": {"type": "json_schema", "schema": SCHEMA}},
)
data = json.loads(maandishi(r))
print(data)
print("Hisia:", data["hisia"], "| Alama:", data["alama"])

Matokeo (takriban):

text
{'hisia': 'wastani', 'alama': 3, 'muhtasari': 'Chakula kilikuwa kitamu lakini huduma ilichelewa sana.'}
Hisia: wastani | Alama: 3

Sema: "Sasa tuna dict ya Python. Tunaweza kuhifadhi kwenye database ya SQLModel tuliyojifunza!"

Hatua 2: Tool use — tool_demo.py

python
# tool_demo.py
import json
from msaidizi import client, maandishi

MODEL = "claude-haiku-5-5"

# "Database" yetu ndogo ya duka
BEI_ZA_DUKA = {"sukari": 3200, "mchele": 2800, "unga": 1800, "mafuta": 4500}  # TSh kwa kilo/lita


def pata_bei(bidhaa: str) -> str:
    bei = BEI_ZA_DUKA.get(bidhaa.lower())
    if bei is None:
        return json.dumps({"kosa": f"Bidhaa '{bidhaa}' haipo dukani"})
    return json.dumps({"bidhaa": bidhaa, "bei_tsh": bei})


TOOLS = [{
    "name": "pata_bei",
    "description": "Pata bei ya sasa ya bidhaa moja kwenye duka la Mama Neema (TSh kwa kilo au lita).",
    "input_schema": {
        "type": "object",
        "properties": {"bidhaa": {"type": "string", "description": "Jina la bidhaa, mf. sukari"}},
        "required": ["bidhaa"],
    },
}]

messages = [{"role": "user", "content": "Nikinunua kilo 2 za sukari na kilo 3 za mchele, nitalipa kiasi gani?"}]

while True:
    response = client.messages.create(
        model=MODEL, max_tokens=2048, tools=TOOLS, messages=messages,
        system="Wewe ni muuzaji wa duka la Mama Neema. Tumia tool pata_bei kupata bei. Jibu kwa Kiswahili.",
    )
    if response.stop_reason != "tool_use":
        break

    messages.append({"role": "assistant", "content": response.content})
    matokeo = []
    for block in response.content:
        if block.type == "tool_use":
            print(f"  [Claude anaomba tool: {block.name}({block.input})]")
            matokeo.append({
                "type": "tool_result",
                "tool_use_id": block.id,
                "content": pata_bei(**block.input),
            })
    messages.append({"role": "user", "content": matokeo})

print(maandishi(response))

Matokeo (takriban):

text
  [Claude anaomba tool: pata_bei({'bidhaa': 'sukari'})]
  [Claude anaomba tool: pata_bei({'bidhaa': 'mchele'})]
Sukari kilo 2 × 3,200 = 6,400 na mchele kilo 3 × 2,800 = 8,400. Jumla utalipa TSh 14,800.

Eleza: "Claude aliomba tools mbili kwa mara moja. Sisi ndio tuliziendesha. Matokeo yote tunayarudisha kwenye ujumbe MMOJA wa user."

Hatua 3: FastAPI — main.py

python
# main.py
import json

import anthropic
from dotenv import load_dotenv
from fastapi import FastAPI, HTTPException
from fastapi.responses import StreamingResponse
from pydantic import BaseModel, Field

load_dotenv()
client = anthropic.Anthropic()
MODEL = "claude-haiku-5-5"
SYSTEM = "Wewe ni msaidizi wa Kiswahili. Jibu kwa ufupi na kwa usahihi. Kama hujui, sema hujui."

app = FastAPI(title="AI API ya Kiswahili (Wiki 14)")


class Swali(BaseModel):
    swali: str = Field(min_length=1, max_length=2000)


class Maoni(BaseModel):
    maandishi: str = Field(min_length=1, max_length=3000)


def maandishi(response) -> str:
    return "".join(b.text for b in response.content if b.type == "text")


def ita_claude(**kwargs):
    """Ita Claude na badilisha errors kuwa HTTPException."""
    try:
        return client.messages.create(model=MODEL, **kwargs)
    except anthropic.AuthenticationError:
        raise HTTPException(status_code=500, detail="Tatizo la usanidi wa server (API key).")
    except anthropic.RateLimitError:
        raise HTTPException(status_code=429, detail="Maombi mengi mno. Jaribu tena baadaye.")
    except anthropic.APIConnectionError:
        raise HTTPException(status_code=503, detail="Huduma ya AI haipatikani kwa sasa.")
    except anthropic.APIStatusError as e:
        raise HTTPException(status_code=502, detail=f"Hitilafu ya huduma ya AI ({e.status_code}).")


@app.post("/uliza")
def uliza(s: Swali):
    r = ita_claude(max_tokens=1024, system=SYSTEM,
                   output_config={"effort": "low"},
                   messages=[{"role": "user", "content": s.swali}])
    if r.stop_reason == "refusal":
        raise HTTPException(status_code=422, detail="Samahani, siwezi kujibu ombi hilo.")
    return {
        "jibu": maandishi(r),
        "tokens": {"input": r.usage.input_tokens, "output": r.usage.output_tokens},
    }


@app.post("/uliza-stream")
def uliza_stream(s: Swali):
    def vipande():
        with client.messages.stream(model=MODEL, max_tokens=1024, system=SYSTEM,
                                    output_config={"effort": "low"},
                                    messages=[{"role": "user", "content": s.swali}]) as stream:
            for text in stream.text_stream:
                yield text
    return StreamingResponse(vipande(), media_type="text/plain; charset=utf-8")


SCHEMA_MAONI = {
    "type": "object",
    "properties": {
        "hisia": {"type": "string", "enum": ["chanya", "hasi", "wastani"]},
        "alama": {"type": "integer"},
        "muhtasari": {"type": "string"},
    },
    "required": ["hisia", "alama", "muhtasari"],
    "additionalProperties": False,
}


@app.post("/chambua-maoni")
def chambua_maoni(m: Maoni):
    r = ita_claude(
        max_tokens=1024,
        system="Chambua maoni ya mteja. alama ni 1 hadi 5. Muhtasari kwa Kiswahili, sentensi moja.",
        messages=[{"role": "user", "content": m.maandishi}],
        output_config={"format": {"type": "json_schema", "schema": SCHEMA_MAONI}},
    )
    return json.loads(maandishi(r))
powershell
uvicorn main:app --reload

Jaribu kwenye http://127.0.0.1:8000/docs, au PowerShell:

powershell
Invoke-RestMethod -Uri http://127.0.0.1:8000/uliza -Method Post -ContentType "application/json; charset=utf-8" -Body '{"swali": "Mji mkuu wa Tanzania ni upi?"}'

Matokeo:

text
jibu                                   tokens
----                                   ------
Mji mkuu wa Tanzania ni Dodoma.        @{input=40; output=12}

Kujaribu streaming, tengeneza mteja_stream.py (terminal nyingine, venv ikiwa imewashwa):

python
# mteja_stream.py
import httpx

with httpx.stream("POST", "http://127.0.0.1:8000/uliza-stream",
                  json={"swali": "Nieleze mzunguko wa maji kwa aya moja."}, timeout=60) as r:
    for kipande in r.iter_text():
        print(kipande, end="", flush=True)
print()

Hatua 4: Tests bila kutumia pesa — test_main.py

python
# test_main.py
import json
from types import SimpleNamespace

from fastapi.testclient import TestClient

import main


def jibu_bandia(text: str):
    """Tengeneza object inayofanana na jibu la Claude."""
    return SimpleNamespace(
        content=[SimpleNamespace(type="text", text=text)],
        stop_reason="end_turn",
        usage=SimpleNamespace(input_tokens=10, output_tokens=5),
    )


class ClientBandia:
    def __init__(self, text):
        self.messages = SimpleNamespace(create=lambda **kwargs: jibu_bandia(text))


def test_uliza(monkeypatch):
    monkeypatch.setattr(main, "client", ClientBandia("Dodoma"))
    r = TestClient(main.app).post("/uliza", json={"swali": "Mji mkuu?"})
    assert r.status_code == 200
    assert r.json()["jibu"] == "Dodoma"
    assert r.json()["tokens"] == {"input": 10, "output": 5}


def test_swali_tupu_linakataliwa():
    r = TestClient(main.app).post("/uliza", json={"swali": ""})
    assert r.status_code == 422


def test_chambua_maoni(monkeypatch):
    data = {"hisia": "chanya", "alama": 5, "muhtasari": "Mteja ameridhika."}
    monkeypatch.setattr(main, "client", ClientBandia(json.dumps(data)))
    r = TestClient(main.app).post("/chambua-maoni", json={"maandishi": "Nimefurahi sana!"})
    assert r.status_code == 200
    assert r.json()["hisia"] == "chanya"
powershell
pytest -v

Sema: "Tests hizi hazitumii internet wala pesa, na zinapita kila mara. Tunapima code YETU, si Claude."

Hatua 5 (hiari, onyesha tu): Ollama — mbadala wa bure

Kama una muda na kompyuta ina RAM ≥ 8GB: sakinisha Ollama kutoka https://ollama.com/download, kisha:

powershell
ollama pull llama3.2
ollama run llama3.2 "Habari, jina lako nani?"

Kwa Python: pip install ollama kisha ollama.chat(model="llama3.2", messages=[{"role": "user", "content": "..."}]). Mwambie: ni bure na data haitoki kwenye kompyuta, lakini ni polepole zaidi na Kiswahili chake ni dhaifu zaidi.

Kwa wanaotaka zaidi (kwa model ya Sonnet tu — taja, usisisitize): Kwa claude-sonnet-5-5, Anthropic inapendekeza kuwasha "fallback" ya server: ombi likikataliwa kwa sababu za usalama (mara chache, hata kwa maswali halali), API inajaribu model nyingine moja kwa moja:

python
r = client.beta.messages.create(
    model="claude-sonnet-5-5",
    max_tokens=2048,
    betas=["server-side-fallback-2026-07-01"],
    fallbacks="default",
    messages=[{"role": "user", "content": "Eleza jinsi chanjo zinavyofanya kazi."}],
)
print(r.stop_reason, maandishi(r))

Maswali ya kumuuliza mwanafunzi

  1. Swali: Nani anaendesha function pata_bei — Claude au sisi? Jibu: Sisi. Claude anaomba tu kupitia block ya tool_use.
  2. Swali: tool_use_id inatumika kwa nini? Jibu: Kuunganisha kila tool_result na ombi la tool husika.
  3. Swali: Faida ya structured output kuliko kumwambia tu "jibu kwa JSON"? Jibu: Inahakikisha JSON halali inayolingana na schema; hakuna kuvunjika kwa json.loads.
  4. Swali: Kwa nini max_length=2000 kwenye Swali? Jibu: Kuzuia maombi makubwa mno yanayoongeza gharama au matumizi mabaya.
  5. Swali: Kwa nini tests zinatumia client bandia? Jibu: Hazitumii pesa/internet, ni za haraka, na majibu hayabadiliki.
  6. Swali: Faida na hasara ya Ollama? Jibu: Faida: bure, faragha, bila internet. Hasara: inahitaji kompyuta yenye nguvu, polepole, ubora wa chini (hasa Kiswahili).

Makosa ya kawaida

Kosa Ujumbe Suluhisho
Kusahau kurudisha tool_result Model inajibu bila data / loop haiishi Kila tool_use lazima ipate tool_result yenye tool_use_id ile ile
tool_result kwenye ujumbe tofauti tofauti Inafanya kazi lakini si sahihi Weka matokeo yote kwenye ujumbe MMOJA wa user
Schema bila additionalProperties: False BadRequestError: 400 ... additionalProperties Ongeza "additionalProperties": False na "required"
json.loads kwenye response.content TypeError: the JSON object must be str Toa maandishi kwanza (maandishi(r))
Kujaribu kulazimisha tool kwa tool_choice={"type": "any"} BadRequestError: 400 ... tool_choice ... not supported for this model Acha default (auto) na mwambie model kwenye prompt atumie tool
Invoke-RestMethod na Kiswahili Herufi zinaharibika Ongeza charset=utf-8 kwenye -ContentType, au tumia /docs
Tests zinaita API halisi Polepole + gharama monkeypatch.setattr(main, "client", ClientBandia(...))

Zoezi darasani

Zoezi 3.1: Ongeza endpoint POST /tafsiri inayopokea {"maandishi": "...", "lugha": "Kiingereza"} na kurudisha {"tafsiri": "..."}. lugha iwe moja ya: Kiswahili, Kiingereza, Kifaransa (tumia Literal).

Zoezi 3.2: Ongeza tool ya pili ongeza_bidhaa_kwenye_kikapu(bidhaa, kilo) kwenye tool_demo.py inayohifadhi kwenye list KIKAPU, kisha uulize: "Niwekee kilo 2 za unga na lita 1 ya mafuta kwenye kikapu, kisha niambie jumla."

Zoezi 3.3: Andika test ya /tafsiri kwa client bandia.

Jibu
python
# 3.1 — ongeza kwenye main.py
from typing import Literal

class Tafsiri(BaseModel):
    maandishi: str = Field(min_length=1, max_length=2000)
    lugha: Literal["Kiswahili", "Kiingereza", "Kifaransa"]


@app.post("/tafsiri")
def tafsiri(t: Tafsiri):
    r = ita_claude(
        max_tokens=1024,
        system="Wewe ni mtafsiri. Rudisha tafsiri tu, bila maelezo ya ziada.",
        output_config={"effort": "low"},
        messages=[{"role": "user", "content": f"Tafsiri kwenda {t.lugha}:\n\n{t.maandishi}"}],
    )
    return {"tafsiri": maandishi(r).strip()}
python
# 3.2 — tool_demo2.py
import json
from msaidizi import client, maandishi

BEI_ZA_DUKA = {"sukari": 3200, "mchele": 2800, "unga": 1800, "mafuta": 4500}
KIKAPU = []


def pata_bei(bidhaa: str) -> str:
    bei = BEI_ZA_DUKA.get(bidhaa.lower())
    return json.dumps({"bidhaa": bidhaa, "bei_tsh": bei} if bei else {"kosa": "haipo"})


def ongeza_bidhaa_kwenye_kikapu(bidhaa: str, kilo: float) -> str:
    if bidhaa.lower() not in BEI_ZA_DUKA:
        return json.dumps({"kosa": f"{bidhaa} haipo dukani"})
    KIKAPU.append({"bidhaa": bidhaa.lower(), "kilo": kilo})
    return json.dumps({"sawa": True, "kikapu": KIKAPU})


FUNCTIONS = {"pata_bei": pata_bei, "ongeza_bidhaa_kwenye_kikapu": ongeza_bidhaa_kwenye_kikapu}

TOOLS = [
    {"name": "pata_bei", "description": "Pata bei ya bidhaa (TSh kwa kilo/lita).",
     "input_schema": {"type": "object", "properties": {"bidhaa": {"type": "string"}},
                      "required": ["bidhaa"]}},
    {"name": "ongeza_bidhaa_kwenye_kikapu", "description": "Weka bidhaa kwenye kikapu cha mteja.",
     "input_schema": {"type": "object",
                      "properties": {"bidhaa": {"type": "string"},
                                     "kilo": {"type": "number", "description": "kilo au lita"}},
                      "required": ["bidhaa", "kilo"]}},
]

messages = [{"role": "user", "content":
             "Niwekee kilo 2 za unga na lita 1 ya mafuta kwenye kikapu, kisha niambie jumla."}]
while True:
    r = client.messages.create(model="claude-haiku-5-5", max_tokens=2048, tools=TOOLS,
                               messages=messages,
                               system="Wewe ni muuzaji wa duka. Tumia tools. Jibu kwa Kiswahili.")
    if r.stop_reason != "tool_use":
        break
    messages.append({"role": "assistant", "content": r.content})
    messages.append({"role": "user", "content": [
        {"type": "tool_result", "tool_use_id": b.id, "content": FUNCTIONS[b.name](**b.input)}
        for b in r.content if b.type == "tool_use"
    ]})

print(maandishi(r))
print("KIKAPU:", KIKAPU)   # unga 2 (3,600) + mafuta 1 (4,500) = 8,100
python
# 3.3 — ongeza kwenye test_main.py
def test_tafsiri(monkeypatch):
    monkeypatch.setattr(main, "client", ClientBandia("Good morning"))
    r = TestClient(main.app).post("/tafsiri", json={"maandishi": "Habari za asubuhi", "lugha": "Kiingereza"})
    assert r.status_code == 200
    assert r.json() == {"tafsiri": "Good morning"}


def test_tafsiri_lugha_isiyokubalika():
    r = TestClient(main.app).post("/tafsiri", json={"maandishi": "Habari", "lugha": "Kichina"})
    assert r.status_code == 422

Kazi ya Nyumbani

(Inatakiwa: kipindi kijacho)

Mradi mdogo: "Msaidizi wa Mapishi"

  1. CLI mapishi.py: chatbot yenye system prompt ya "Mpishi wa vyakula vya Kitanzania", inayotumia streaming na historia, yenye amri toka, upya, gharama.
  2. FastAPI POST /mapishi inayopokea {"viungo": ["mchele", "nazi", "samaki"]} (list isiyo tupu, isizidi vitu 15) na kurudisha JSON yenye muundo huu kwa structured output:

    json
    {"jina_la_chakula": "...", "muda_dakika": 40, "hatua": ["...", "..."]}
    
  3. Test 2 za pytest (kwa client bandia): moja ya jibu sahihi, moja ya list tupu kukataliwa (422).

  4. Hakikisha .env HAIKO kwenye git status, na kuna .env.example. Commit.
Jibu
python
# mapishi.py
from dotenv import load_dotenv
import anthropic

load_dotenv()
client = anthropic.Anthropic()
MODEL = "claude-haiku-5-5"
SYSTEM = ("Wewe ni mpishi hodari wa vyakula vya Kitanzania (pilau, ugali, wali wa nazi, "
          "mishkaki, chapati...). Jibu kwa Kiswahili, kwa hatua zilizo wazi na fupi.")


def main():
    historia, t_in, t_out = [], 0, 0
    while True:
        u = input("Wewe: ").strip()
        if not u:
            continue
        if u == "toka":
            break
        if u == "upya":
            historia = []
            continue
        if u == "gharama":
            usd = (t_in * 0.10 + t_out * 0.50) / 1_000_000
            print(f"${usd:.6f} ≈ TSh {usd * 2600:.2f}")
            continue
        historia.append({"role": "user", "content": u})
        print("Mpishi: ", end="", flush=True)
        with client.messages.stream(model=MODEL, max_tokens=2048, system=SYSTEM,
                                    messages=historia) as s:
            for t in s.text_stream:
                print(t, end="", flush=True)
            f = s.get_final_message()
        print("\n")
        historia.append({"role": "assistant", "content": f.content})
        t_in += f.usage.input_tokens
        t_out += f.usage.output_tokens


if __name__ == "__main__":
    main()
python
# api_mapishi.py   ->  uvicorn api_mapishi:app --reload
import json
import anthropic
from dotenv import load_dotenv
from fastapi import FastAPI
from pydantic import BaseModel, Field

load_dotenv()
client = anthropic.Anthropic()
app = FastAPI()

SCHEMA = {
    "type": "object",
    "properties": {
        "jina_la_chakula": {"type": "string"},
        "muda_dakika": {"type": "integer"},
        "hatua": {"type": "array", "items": {"type": "string"}},
    },
    "required": ["jina_la_chakula", "muda_dakika", "hatua"],
    "additionalProperties": False,
}


class Viungo(BaseModel):
    viungo: list[str] = Field(min_length=1, max_length=15)


@app.post("/mapishi")
def mapishi(v: Viungo):
    r = client.messages.create(
        model="claude-haiku-5-5",
        max_tokens=2048,
        system="Pendekeza chakula kimoja cha Kitanzania kwa viungo ulivyopewa. Kwa Kiswahili.",
        messages=[{"role": "user", "content": "Viungo: " + ", ".join(v.viungo)}],
        output_config={"format": {"type": "json_schema", "schema": SCHEMA}},
    )
    text = "".join(b.text for b in r.content if b.type == "text")
    return json.loads(text)
python
# test_api_mapishi.py
import json
from types import SimpleNamespace
from fastapi.testclient import TestClient
import api_mapishi


def test_mapishi(monkeypatch):
    data = {"jina_la_chakula": "Wali wa nazi na samaki", "muda_dakika": 45, "hatua": ["Osha mchele"]}
    jibu = SimpleNamespace(content=[SimpleNamespace(type="text", text=json.dumps(data))])
    bandia = SimpleNamespace(messages=SimpleNamespace(create=lambda **kw: jibu))
    monkeypatch.setattr(api_mapishi, "client", bandia)
    r = TestClient(api_mapishi.app).post("/mapishi", json={"viungo": ["mchele", "nazi", "samaki"]})
    assert r.status_code == 200
    assert r.json()["muda_dakika"] == 45


def test_viungo_tupu():
    r = TestClient(api_mapishi.app).post("/mapishi", json={"viungo": []})
    assert r.status_code == 422

Ukiulizwa swali usilolijua

Sema: "Hilo ni swali zuri; teknolojia hii inabadilika haraka sana, tuangalie documentation rasmi pamoja." Tafuta: "Claude API python", au ujumbe kamili wa error. Usibuni majina ya parameters — angalia docs.


Checklist ya mwisho wa wiki

  • Anaeleza LLM ni nini na token ni nini, na kwa nini Kiswahili kinaweza kuwa na tokens zaidi
  • API key iko .env, .env iko .gitignore, .env.example ipo — na hajaonyesha key kwenye screen
  • Anaita client.messages.create na kutoa maandishi kwa kuchuja blocks za "text"
  • Anasoma stop_reason na usage, na anahesabu gharama
  • Anashika errors (AuthenticationError, RateLimitError, APIConnectionError)
  • Anatumia system prompt na anaeleza tofauti yake na ujumbe wa user
  • Chatbot yake ya CLI inakumbuka historia na inatumia streaming
  • Anaeleza mzunguko wa tool use (tool_use → tunaendesha → tool_result)
  • Anapata JSON yenye muundo kwa output_config + schema
  • Ana endpoint ya FastAPI /uliza inayofanya kazi, na tests zinazotumia client bandia
  • Anajua mbinu 3 za kupunguza gharama na anajua Ollama ni nini