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:
- Fungua akaunti kwenye Claude Console: https://platform.claude.com (zamani console.anthropic.com).
- Weka salio (credits) dogo — mf. USD 5 inatosha kwa wiki nzima ya mazoezi kwa model ya bei nafuu.
- Weka kikomo cha matumizi (spend limit) kwenye Console → Settings → Limits, ili mwanafunzi asitumie pesa nyingi kwa bahati mbaya.
- Tengeneza API key (Console → API Keys → Create Key). Ipe jina kama
mwanafunzi-wiki14. Nakili mara moja — haionyeshwi tena. - 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:
- Python SDK quickstart: https://platform.claude.com/docs/en/api/client-sdks
- Messages API: https://platform.claude.com/docs/en/api/messages
- Streaming: https://platform.claude.com/docs/en/build-with-claude/streaming
- Tool use: https://platform.claude.com/docs/en/agents-and-tools/tool-use/overview
- Bei (pricing): https://platform.claude.com/docs/en/about-claude/pricing
-
Sakinisha:
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-5karibu muda wote. Tutaonyeshaclaude-sonnet-5-5mara 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...)
- Kueleza LLM (Large Language Model) ni nini kwa lugha rahisi, na token ni nini.
- Kuandika prompt nzuri (wazi, yenye muktadha, mfano wa jibu).
- Kuhifadhi API key kwenye
.env, kuisoma kwapython-dotenv, na kuhakikisha.envhaiingii Git. - Kuita Claude API kwa
anthropicSDK (client.messages.create) na kusoma jibu,stop_reasonnausage. - Kuhesabu gharama ya ombi (request) kutokana na tokens.
- Kutumia system prompt na historia ya mazungumzo (multi-turn).
- Kutumia streaming ili jibu lionekane neno kwa neno.
- Kuelewa dhana ya tool use na structured JSON output.
- Kujenga chatbot ya Kiswahili kwenye terminal, na endpoint ya FastAPI
POST /uliza. - Kujua mbadala wa bure (Ollama — models za ndani ya kompyuta).
Kipindi 1 — LLM ni nini? API key na ombi la kwanza
Mtiririko wa kipindi
- Marudio ya kazi ya nyumbani ya Wiki 13 (alama za mtihani)
- LLM ni nini? Tokens, prompts, tofauti na ML ya Wiki 13
- API keys,
.env,.gitignore— usalama kwanza - Live coding: ombi la kwanza, kusoma jibu, usage na gharama
- Makosa (errors) na jinsi ya kuyashika
- Zoezi darasani
- 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.gitignorekabla ya commit ya kwanza. python-dotenvinasoma.envna kuweka thamani kwenye "environment variables". SDK yaanthropicinatafuta yenyewe variable inayoitwaANTHROPIC_API_KEY.
Muundo wa ombi (request):
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 inatype == "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 kudhanicontent[0]ni maandishi.response.stop_reason— kwa nini ilisimama:"end_turn"(imemaliza kawaida),"max_tokens"(imekatwa — ongezamax_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
.envyako mapema, na usiifungue ukiwa unashare.
Hatua 1: .gitignore KWANZA
git init
Tengeneza faili .gitignore:
venv/ .env __pycache__/
Hatua 2: .env na .env.example
Faili .env (lisilo la kushare):
ANTHROPIC_API_KEY=sk-ant-weka-key-yako-hapa
Faili .env.example (hili linaweza ku-commitiwa — linaonyesha wengine variables zinazohitajika bila siri):
ANTHROPIC_API_KEY=
Thibitisha:
git status
.envhaipaswi kuonekana kwenye orodha. Ikionekana,.gitignoreina kosa.
Hatua 3: Ombi la kwanza — kwanza.py
# 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)
python kwanza.py
Matokeo (maandishi yatatofautiana kila mara — hilo ni la kawaida):
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
# 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):
[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)
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_tokensni bure.
Hatua 6: Kupunguza "kufikiri" kwa maswali rahisi
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)
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
.envkuwa 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
-
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.
-
Swali: Kwa nini tusiandike API key ndani ya
kwanza.py? Jibu: Tukicommit/kushare code, key inavuja na mtu anaweza kutumia salio letu. Tunaiweka.enviliyo kwenye.gitignore. -
Swali:
stop_reason == "max_tokens"ina maana gani na tufanye nini? Jibu: Jibu limekatwa kabla ya kumaliza; ongezamax_tokensau omba jibu fupi zaidi. -
Swali: Kwa nini tusitumie
response.content[0].textmoja kwa moja? Jibu:contentni list ambayo inaweza kuwa na blocks za aina nyingine (mf. thinking) mwanzoni; tunachujatype == "text". -
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
# 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
- Marudio ya kazi ya nyumbani (Kipindi 1)
- System prompt — "kumpa model kazi/utambulisho"
- Multi-turn: model haikumbuki — sisi tunatuma historia
- Chatbot ya Kiswahili kwenye terminal
- Streaming — jibu neno kwa neno
- Zoezi darasani
- 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:
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:
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
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
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
# 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()
python chatbot.py
Mfano wa mazungumzo:
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
- Swali: Tofauti ya
systemna ujumbe wauser? Jibu: System ni maelekezo ya jumla ya tabia/utambulisho kwa mazungumzo yote; user ni ujumbe wa mtumiaji wa kila zamu. - Swali: Kwa nini input tokens zinaongezeka kila zamu? Jibu: Tunatuma historia yote kila ombi, kwa sababu model haikumbuki.
- Swali: Faida mbili za streaming? Jibu: Mtumiaji anaona jibu mara moja (uzoefu bora); majibu marefu hayakwami kwa timeout.
- Swali: Amri ya
upyainaokoa nini? Jibu: Inafuta historia, hivyo inapunguza tokens (gharama) na kuanza mada mpya. - Swali: Kwa nini
historia.pop()kwenyeexcept? 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
# 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
- Marudio ya kazi ya nyumbani (Kipindi 2)
- Structured JSON output — kupata data, si maneno tu
- Tool use — model "inaomba" kutumia function zetu
- FastAPI:
POST /uliza,POST /uliza-stream,POST /chambua-maoni - Kupima endpoint bila kutumia pesa (mock/monkeypatch)
- Gharama, usalama, na mbadala wa bure (Ollama)
- Zoezi darasani
- 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:
- Tunatuma swali + maelezo ya tools (jina, maelezo,
input_schema). - Model ikiamua inahitaji tool, inarudisha
stop_reason == "tool_use"na block yatool_use(jina + input, mf.{"bidhaa": "sukari"}). - Sisi tunaendesha function yetu ya Python na kupata matokeo.
- Tunarudisha matokeo kama block ya
tool_result(pamoja natool_use_idile ile) kwenye ujumbe wauser. - 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_lengthya swali (kuzuia mtu kutuma maandishi makubwa mno = gharama),max_tokensya jibu. - Badilisha errors za Anthropic kuwa
HTTPExceptionzenye 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
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):
{'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
# 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):
[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
# 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))
uvicorn main:app --reload
Jaribu kwenye http://127.0.0.1:8000/docs, au 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:
jibu tokens
---- ------
Mji mkuu wa Tanzania ni Dodoma. @{input=40; output=12}
Kujaribu streaming, tengeneza mteja_stream.py (terminal nyingine, venv ikiwa imewashwa):
# 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
# 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"
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:
ollama pull llama3.2 ollama run llama3.2 "Habari, jina lako nani?"
Kwa Python:
pip install ollamakishaollama.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:
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
- Swali: Nani anaendesha function
pata_bei— Claude au sisi? Jibu: Sisi. Claude anaomba tu kupitia block yatool_use. - Swali:
tool_use_idinatumika kwa nini? Jibu: Kuunganisha kilatool_resultna ombi la tool husika. - Swali: Faida ya structured output kuliko kumwambia tu "jibu kwa JSON"? Jibu: Inahakikisha JSON halali inayolingana na schema; hakuna kuvunjika kwa
json.loads. - Swali: Kwa nini
max_length=2000kwenyeSwali? Jibu: Kuzuia maombi makubwa mno yanayoongeza gharama au matumizi mabaya. - Swali: Kwa nini tests zinatumia client bandia? Jibu: Hazitumii pesa/internet, ni za haraka, na majibu hayabadiliki.
- 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
# 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()}
# 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
# 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"
- CLI
mapishi.py: chatbot yenye system prompt ya "Mpishi wa vyakula vya Kitanzania", inayotumia streaming na historia, yenye amritoka,upya,gharama. -
FastAPI
POST /mapishiinayopokea{"viungo": ["mchele", "nazi", "samaki"]}(list isiyo tupu, isizidi vitu 15) na kurudisha JSON yenye muundo huu kwa structured output:{"jina_la_chakula": "...", "muda_dakika": 40, "hatua": ["...", "..."]}
-
Test 2 za pytest (kwa client bandia): moja ya jibu sahihi, moja ya list tupu kukataliwa (422).
- Hakikisha
.envHAIKO kwenyegit status, na kuna.env.example. Commit.
Jibu
# 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()
# 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)
# 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
- Claude API docs: https://platform.claude.com/docs/en/api/overview
- Python SDK (GitHub): https://github.com/anthropics/anthropic-sdk-python
- Prompt engineering: https://platform.claude.com/docs/en/build-with-claude/prompt-engineering/overview
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,.enviko.gitignore,.env.exampleipo — na hajaonyesha key kwenye screen - Anaita
client.messages.createna kutoa maandishi kwa kuchuja blocks za"text" - Anasoma
stop_reasonnausage, na anahesabu gharama - Anashika errors (
AuthenticationError,RateLimitError,APIConnectionError) - Anatumia
systemprompt na anaeleza tofauti yake na ujumbe wauser - 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
/ulizainayofanya kazi, na tests zinazotumia client bandia - Anajua mbinu 3 za kupunguza gharama na anajua Ollama ni nini