Dan 10/28: Struktura zahteva i provider routing
Vreme potrebno: 90 – 180 minuta
Gde je Učitelj Vasa danas?
Učitelj Vasa ima funkcionalan web API sa endpoint-ima koji prikazuju informacije o providerima, njihov status i osnovne statistike. Kroz /pitaj endpoint može da prima pitanja, ali trenutno sve zahteve obrađuje isto – jednostavno prosleđuje OpenAI ili Gemini servisu koji je konfigurisan. Ne razlikuje da li korisnik traži generisanje koda, objašnjenje koncepta ili analizu greške. Danas menjamo to!
Cilj današnje lekcije
Danas ćeš naučiti kako da kreiraš strukturirane tipove zahteva koji jasno definišu šta korisnik traži, implementiraš inteligentno rutiranje koje bira najbolji provider za svaki tip zahteva, i standardizuješ request format za lakšu obradu i proširivanje. Nakon ove lekcije, Učitelj Vasa će moći da prepozna tip zahteva i automatski izabere najadekvatniji AI servis!
Predznanja
- Funkcionalan FastAPI server sa provider endpoint-ima (Dan 8-9)
- Multi-provider sistem sa factory pattern-om (Dan 4)
- Razumevanje dictionary struktura u Python-u
- Osnovno razumevanje HTTP request/response ciklusa
- BaseAIService interfejs (Dan 4)
Glavni sadržaj
Zašto su strukturirani zahtevi važni?
Pre nego što krenemo sa implementacijom, razumimo zašto nam trebaju različiti tipovi zahteva.
📚 NIVO 1 – Osnovno objašnjenje
Zamisli restoran sa više kuvara:
- Jedan kuvar je specijalista za paste
- Drugi je ekspert za roštilj
- Treći pravi najbolje salate
Kada gost naruči, konobar (naš router) mora da zna kome da prosledi narudžbinu. Isto važi za AI servise:
- OpenAI možda bolje generiše kod
- Gemini možda brže odgovara na jednostavna pitanja
- Neki budući servis će možda biti najbolji za analizu
Umesto da sve šaljemo istom “kuvaru”, trebamo sistem koji razume šta se traži i bira pravog eksperta!
🚀 NIVO 2 – Dublje razumevanje
Strukturirani zahtevi omogućavaju:
- Type Safety – znamo tačno šta očekujemo
- Routing Logic – možemo da biramo provider po tipu
- Optimization – različiti parametri za različite zadatke
- Analytics – lakše praćenje šta korisnici najviše traže
- Future Proofing – lako dodavanje novih tipova
💡 PRO TIP: Strukturirani zahtevi nisu samo o rutiranju – oni su o razumevanju korisničkih potreba. Kada eksplicitno definišeš tipove kao “code_generation” ili “concept_explanation”, ti zapravo gradiš taksonomiju znanja. Ovo kasnije omogućava napredne funkcije poput automatskog tagovanja, preporuka sličnih pitanja, ili čak prediktivnog keširanja odgovora.
🔍 UVID: Ovaj pristup je primer “Domain-Driven Design” (DDD) principa. Umesto da razmišljamo o tehničkim detaljima (koji API poziv), fokusiramo se na domenski jezik (šta korisnik zaista želi). Tipovi zahteva koje definišemo postaju “Ubiquitous Language” – zajednički rečnik koji koriste i developeri i korisnici. Ovo drastično smanjuje nesporazume i olakšava komunikaciju o funkcionalnostima.
🎈 ZABAVNA ČINJENICA: Google interno koristi preko 200 različitih “intent types” za svoju pretragu! Od “navigational” (želim da odem na sajt) do “transactional” (želim nešto da kupim). Njihov routing sistem odlučuje da li da prikaže mapu, shopping rezultate ili knowledge panel na osnovu prepoznatog tipa!
🌐 GOOGLE CONNECTION: Sistem koji gradimo sa RequestAnalyzer-om je minijaturna verzija onoga što Google Cloud Dialogflow radi. U Dialogflow-u, vi definišete “Intents” (kao naši RequestType-ovi) i “Training Phrases” (kao naši TYPE_KEYWORDS). Kada korisnik pošalje poruku, Dialogflow koristi Google-ove pre-trenirane modele da prepozna pravi intent sa visokom preciznošću, daleko iznad onoga što se može postići prostim pretraživanjem ključnih reči. Naš sistem je odličan temelj za razumevanje ovih naprednih koncepata.
Kreiranje strukture zahteva
Počnimo sa definisanjem jasnih tipova zahteva koje Učitelj Vasa može da obrađuje.
📚 NIVO 1 – Osnovno objašnjenje
Tipovi zahteva su kao kategorije pitanja:
- Chat – obična konverzacija (“Kako si?”, “Objasni mi…”)
- Code – generisanje koda (“Napiši funkciju koja…”)
- Debug – pomoć sa greškama (“Zašto ovaj kod ne radi?”)
- Explain – detaljno objašnjenje (“Kako funkcioniše…”)
- Review – pregled koda (“Da li je ovaj kod dobar?”)
Svaki tip može imati svoje posebne postavke i preferiranog providera.
🚀 NIVO 2 – Dublje razumevanje
Struktura zahteva treba da sadrži:
- request_type – eksplicitni tip
- content – glavno pitanje/zahtev
- context – dodatne informacije
- preferences – specifične postavke
- metadata – tracking informacije
💡 PRO TIP: Kad god kreiraš novi request_type, dodaj istoimeni metod get_preferred_provider(). Tako izbegavaš “magijske konstante” i centralizuješ logiku izbora providera – sutra, ako Gemini postane bolji za debug, menjaš samo jedno mesto.
📊 DIJAGRAM: Tok strukturiranog zahteva
[Korisnikov zahtev]
|
v
+------------------+
| Request Analyzer | (Prepoznaje tip)
+------------------+
|
v
+------------------+
| Request Builder | (Kreira strukturu)
+------------------+
|
v
+------------------+
| Router Logic | (Bira provider)
+------------------+
|
[provider A/B/C]
|
v
+------------------+
| Formatted Call | (Optimizovan poziv)
+------------------+
Kreiraj novi folder i fajlove:
mkdir src/web_api/models
touch src/web_api/models/__init__.py
touch src/web_api/models/request_types.py
Kreiraj src/web_api/models/request_types.py:
"""
Definicije tipova zahteva za Učitelja Vasu
Omogućava strukturirano rukovanje različitim vrstama pitanja
"""
from enum import Enum
from typing import Optional, Dict, Any, List
from datetime import datetime
class RequestType(Enum):
"""Tipovi zahteva koje Vasa može da obrađuje."""
CHAT = "chat" # Obična konverzacija
CODE_GENERATION = "code" # Generisanje koda
CODE_DEBUG = "debug" # Debugging pomoć
CONCEPT_EXPLAIN = "explain" # Objašnjenje koncepata
CODE_REVIEW = "review" # Pregled i analiza koda
TRANSLATION = "translate" # Prevod koda između jezika
OPTIMIZATION = "optimize" # Optimizacija koda
def get_description(self) -> str:
"""Vraća opis tipa zahteva."""
descriptions = {
"chat": "Opšta konverzacija i jednostavna pitanja",
"code": "Generisanje novog koda prema specifikaciji",
"debug": "Pomoć pri pronalaženju i rešavanju grešaka",
"explain": "Detaljno objašnjenje programskih koncepata",
"review": "Analiza kvaliteta postojećeg koda",
"translate": "Prevođenje koda između programskih jezika",
"optimize": "Poboljšanje performansi postojećeg koda"
}
return descriptions.get(self.value, "Nepoznat tip")
def get_preferred_provider(self) -> str:
"""Vraća preferiranog providera za ovaj tip."""
# Ovo može biti konfigurisano ili naučeno kroz vreme
preferences = {
"chat": "gemini", # Gemini brži za jednostavne odgovore
"code": "openai", # OpenAI bolji za kod
"debug": "openai", # OpenAI bolji za analizu
"explain": "gemini", # Gemini daje jasnije objašnjenje
"review": "openai", # OpenAI detaljniji review
"translate": "openai", # OpenAI precizniji prevod
"optimize": "openai" # OpenAI bolje optimizuje
}
return preferences.get(self.value, "any")
class RequestContext:
"""Kontekst zahteva sa dodatnim informacijama."""
def __init__(
self,
programming_language: Optional[str] = None,
error_message: Optional[str] = None,
code_snippet: Optional[str] = None,
user_level: Optional[str] = None,
previous_attempts: Optional[List[str]] = None
):
self.programming_language = programming_language
self.error_message = error_message
self.code_snippet = code_snippet
self.user_level = user_level or "intermediate"
self.previous_attempts = previous_attempts or []
def to_dict(self) -> Dict[str, Any]:
"""Konvertuje kontekst u dictionary."""
return {
"programming_language": self.programming_language,
"error_message": self.error_message,
"code_snippet": self.code_snippet,
"user_level": self.user_level,
"previous_attempts": self.previous_attempts
}
def has_code_context(self) -> bool:
"""Proverava da li postoji kod kontekst."""
return bool(self.code_snippet or self.error_message)
class StructuredRequest:
"""Strukturiran zahtev sa svim potrebnim informacijama."""
def __init__(
self,
content: str,
request_type: RequestType,
context: Optional[RequestContext] = None,
preferences: Optional[Dict[str, Any]] = None,
metadata: Optional[Dict[str, Any]] = None
):
self.content = content
self.request_type = request_type
self.context = context or RequestContext()
self.preferences = preferences or {}
self.metadata = metadata or {}
# Automatski dodaj timestamp
self.metadata["timestamp"] = datetime.now().isoformat()
def get_optimized_params(self) -> Dict[str, Any]:
"""Vraća optimizovane parametre za ovaj tip zahteva."""
# Bazni parametri
params = {
"temperature": 0.7,
"max_tokens": 150
}
# Prilagodi prema tipu
if self.request_type == RequestType.CODE_GENERATION:
params["temperature"] = 0.3 # Manja kreativnost za kod
params["max_tokens"] = 300 # Više tokena za kod
elif self.request_type == RequestType.CHAT:
params["temperature"] = 0.8 # Veća kreativnost za chat
params["max_tokens"] = 100 # Kraći odgovori
elif self.request_type == RequestType.CODE_DEBUG:
params["temperature"] = 0.2 # Vrlo precizno za debug
params["max_tokens"] = 250
elif self.request_type == RequestType.CONCEPT_EXPLAIN:
params["temperature"] = 0.6
params["max_tokens"] = 400 # Duže objašnjenje
# Primeni korisničke preference
params.update(self.preferences)
return params
def get_enhanced_prompt(self) -> str:
"""Generiše poboljšani prompt sa kontekstom."""
enhanced = self.content
# Dodaj kontekst ako postoji
if self.context.programming_language:
enhanced = f"[Jezik: {self.context.programming_language}] {enhanced}"
if self.context.error_message:
enhanced += f"\n\nGreška: {self.context.error_message}"
if self.context.code_snippet:
enhanced += f"\n\nKod:\n```\n{self.context.code_snippet}\n```"
if self.context.user_level == "beginner":
enhanced += "\n\n(Napomena: Korisnik je početnik, koristi jednostavne termine)"
return enhanced
def to_dict(self) -> Dict[str, Any]:
"""Konvertuje zahtev u dictionary za serijalizaciju."""
return {
"content": self.content,
"request_type": self.request_type.value,
"context": self.context.to_dict(),
"preferences": self.preferences,
"metadata": self.metadata
}
class RequestAnalyzer:
"""Analizira sirovi zahtev i određuje tip."""
# Ključne reči za prepoznavanje tipova
TYPE_KEYWORDS = {
RequestType.CODE_GENERATION: [
"napiši", "generiši", "kreiraj", "kod za", "funkcij",
"implementiraj", "primer koda", "write", "create", "generate"
],
RequestType.CODE_DEBUG: [
"greška", "error", "ne radi", "problem", "bug", "debug",
"zašto", "exception", "pomaži", "popravi", "fix"
],
RequestType.CONCEPT_EXPLAIN: [
"objasni", "šta je", "kako funkcioniše", "razumem",
"koncept", "teorija", "explain", "what is", "how does"
],
RequestType.CODE_REVIEW: [
"pregled", "review", "da li je dobro", "proveri",
"analiza", "kvalitet", "najbolja praksa", "check"
],
RequestType.TRANSLATION: [
"prevedi", "konvertuj", "iz python u", "translate",
"convert", "prebaci"
],
RequestType.OPTIMIZATION: [
"optimizuj", "brže", "performanse", "optimize",
"faster", "performance", "poboljšaj", "improve"
]
}
@classmethod
def analyze(cls, raw_content: str) -> RequestType:
"""
Analizira sirovi sadržaj i određuje tip zahteva.
Args:
raw_content: Originalni tekst korisnika
Returns:
Prepoznat RequestType
"""
content_lower = raw_content.lower()
# Brojač poena za svaki tip
scores = {req_type: 0 for req_type in RequestType}
# Analiziraj ključne reči
for req_type, keywords in cls.TYPE_KEYWORDS.items():
for keyword in keywords:
if keyword in content_lower:
scores[req_type] += 1
# Dodatni poeni za specifične obrasce
if "```" in raw_content or "def " in raw_content or "class " in raw_content:
scores[RequestType.CODE_DEBUG] += 2
scores[RequestType.CODE_REVIEW] += 1
if "?" in raw_content and any(word in content_lower for word in ["šta", "kako", "zašto"]):
scores[RequestType.CONCEPT_EXPLAIN] += 1
# Pronađi tip sa najviše poena
max_score = max(scores.values())
if max_score > 0:
for req_type, score in scores.items():
if score == max_score:
return req_type
# Default na CHAT
return RequestType.CHAT
@classmethod
def extract_context(cls, raw_content: str) -> RequestContext:
"""
Ekstraktuje kontekst iz sirovog sadržaja.
Args:
raw_content: Originalni tekst
Returns:
RequestContext sa izvučenim informacijama
"""
context = RequestContext()
# Prepoznaj programski jezik
languages = ["python", "javascript", "java", "c++", "c#", "go", "rust"]
content_lower = raw_content.lower()
for lang in languages:
if lang in content_lower:
context.programming_language = lang
break
# Izvuci kod između ``` markera
import re
code_blocks = re.findall(r'```[\w]*\n(.*?)```', raw_content, re.DOTALL)
if code_blocks:
context.code_snippet = code_blocks[0].strip()
# Prepoznaj error poruke
error_patterns = [
r'(Error:.*)',
r'(Exception:.*)',
r'(Traceback.*)',
r'(\w+Error:.*)'
]
for pattern in error_patterns:
match = re.search(pattern, raw_content, re.IGNORECASE)
if match:
context.error_message = match.group(1)
break
return context
@classmethod
def create_structured_request(
cls,
raw_content: str,
force_type: Optional[RequestType] = None,
additional_context: Optional[Dict[str, Any]] = None
) -> StructuredRequest:
"""
Kreira strukturiran zahtev iz sirovog sadržaja.
Args:
raw_content: Originalni tekst
force_type: Opciono forsiraj tip
additional_context: Dodatni kontekst
Returns:
StructuredRequest objekat
"""
# Odredi tip
request_type = force_type or cls.analyze(raw_content)
# Izvuci kontekst
context = cls.extract_context(raw_content)
# Primeni dodatni kontekst ako postoji
if additional_context:
if "user_level" in additional_context:
context.user_level = additional_context["user_level"]
if "programming_language" in additional_context:
context.programming_language = additional_context["programming_language"]
# Kreiraj strukturiran zahtev
return StructuredRequest(
content=raw_content,
request_type=request_type,
context=context
)
# Test funkcionalnost - izvršava se samo ako se fajl pokrene direktno
if __name__ == "__main__":
print("🧪 Test Request Types modula")
print("=" * 50)
# Kreiraj instancu analyzer-a za testiranje
analyzer = RequestAnalyzer()
# Test primeri
test_cases = [
"Kako da sortiram listu u Python-u?",
"Napiši funkciju koja računa fibonacci",
"Zašto mi ovaj kod baca IndexError?",
"Objasni mi šta su closure u JavaScript-u",
"Pregledaj ovaj kod i reci da li je dobar",
"Kako da optimizujem ovu petlju?",
"Zdravo, kako si?"
]
for test in test_cases:
print(f"\nTest: '{test[:50]}...'")
req_type = analyzer.analyze(test)
context = analyzer.extract_context(test)
print(f" Tip: {req_type.value} - {req_type.get_description()}")
print(f" Preferirani provider: {req_type.get_preferred_provider()}")
if context.programming_language:
print(f" Jezik: {context.programming_language}")
# Test strukturiranog zahteva
print("\n" + "=" * 50)
print("Test strukturiranog zahteva:")
complex_request = """
Imam problem sa ovim Python kodom:
```python
def calculate_average(numbers):
return sum(numbers) / len(numbers)
```
Error: ZeroDivisionError: division by zero
Kako da popravim ovu grešku?
"""
# Ovde koristi već definisan analyzer
structured = analyzer.create_structured_request(complex_request)
print(f"\nTip: {structured.request_type.value}")
print(f"Kontekst: {structured.context.to_dict()}")
print(f"Optimizovani parametri: {structured.get_optimized_params()}")
⚡ SAVET ZA OPTIMIZACIJU: Umesto analize ključnih reči u realnom vremenu, razmisli o pre-procesiranju. Možeš kreirati “intent classifier” koji koristi TF-IDF ili čak mali ML model treniran na kategorizovanim pitanjima. Za početak, možeš čuvati sve zahteve sa ručno dodeljenim tipovima i periodično retrenirati classifier.
🎯 ALTERNATIVNO REŠENJE: Ako ti je potrebna veća preciznost bez ML-a, iskoristi regular expression scoring. Svaki pogodak nosi težinu, npr. r"\b(napiši|generiši)\b.*\b(kod|funkciju)\b" = +3. Na ovaj način zadržavaš “pravila na jednom mestu”, ali dobijаš finiju gradaciju poena.
🤔 MINI-KVIZ:
- Zašto
RequestAnalyzerpovećava skor zaCODE_DEBUGkad nađe ““`” u tekstu? - Koji je najsloženiji deo ekstrakcije konteksta i zašto?
(Odgovore potraži u kodu i komentarima.)
🎈 ZABAVNA ČINJENICA: Enum u Python-u je nastao kao side-project Gvidove pauze za kafu 2013. i za samo dve nedelje ušao u PEP 435! Do tada su se svi snalazili sa globalnim konstantama.
Implementacija provider routing logike
Sada ćemo kreirati sistem koji inteligentno bira provider na osnovu tipa zahteva.
📚 NIVO 1 – Osnovno objašnjenje
Provider router je kao GPS za AI pozive – gleda gde treba da stigne (tip zahteva) i bira najbolju rutu (provider). Može da uzme u obzir:
- Koji provider je najbolji za taj tip
- Koji provider trenutno radi
- Koji je brži ili jeftiniji
- Šta korisnik preferira
🚀 NIVO 2 – Dublje razumevanje
Routing logika može biti:
- Static – fiksna pravila (code → OpenAI)
- Dynamic – na osnovu trenutnog stanja
- Learning – uči iz rezultata
- Hybrid – kombinacija pristupa
💡 PRO TIP: Želiš da testiraš strategije bez restartova servera? U SmartProviderRouter dodaj metod set_strategy(strategy: RoutingStrategy) i pozovi ga preko novog /routing/strategy endpoint-a – baš kao hot-swap u production service-mesh-u.
📊 DIJAGRAM: Provider Routing Decision Tree
[Strukturiran Zahtev]
|
v
[Tip zahteva?]
|
+---------+---------+---------+
| | | |
[CODE] [CHAT] [DEBUG] [OTHER]
| | | |
v v v v
[OpenAI?] [Gemini?] [OpenAI?] [Default]
| | | |
[Dostupan?] [Brži?] [Precizniji?] |
| | | |
v v v v
[ODLUKA] [ODLUKA] [ODLUKA] [ODLUKA]
🔍 UVID: Ovaj routing pattern je srž modernog “Service Mesh” pristupa koji koriste Netflix, Uber i drugi tech giganti. Umesto hardkodovanog odredišta, svaki zahtev prolazi kroz “proxy” koji dinamički odlučuje. Ovo omogućava A/B testiranje (10% zahteva ide na novu verziju), “canary deployments” (postepeno uvođenje), i “circuit breaking” (prebacivanje sa servisa koji ne radi). Mi implementiramo mini verziju ovog enterprise patterna!
🎈 ZABAVNA ČINJENICA: Netflix-ov Zuul (API gateway) radi preko 150 000 rutirajućih odluka u sekundi; naša mini-verzija koristi identičan obrazac “strategija → izbor → metapodaci → analitika”, samo na manjem obimu.
Kreiraj src/web_api/models/router.py:
"""
Provider Router za Učitelja Vasu
Inteligentno rutiranje zahteva ka najboljim AI providerima
"""
from typing import Dict, Any, Optional, List, Tuple
from datetime import datetime, timedelta
import random
from web_api.models.request_types import RequestType, StructuredRequest
from utils.config import Config
from utils.performance_tracker import tracker
from utils.circuit_breaker import circuit_registry
class RoutingStrategy:
"""Bazna klasa za routing strategije."""
def select_provider(
self,
request: StructuredRequest,
available_providers: List[str]
) -> str:
"""
Bira provider za zahtev.
Args:
request: Strukturiran zahtev
available_providers: Lista dostupnih providera
Returns:
Ime izabranog providera
"""
raise NotImplementedError
class StaticRoutingStrategy(RoutingStrategy):
"""Statička routing strategija bazirana na tipu zahteva."""
def __init__(self, rules: Optional[Dict[RequestType, str]] = None):
"""
Inicijalizuje sa pravilima rutiranja.
Args:
rules: Mapa tip -> provider
"""
self.rules = rules or self._default_rules()
def _default_rules(self) -> Dict[RequestType, str]:
"""Vraća default pravila rutiranja."""
return {
RequestType.CHAT: "gemini",
RequestType.CODE_GENERATION: "openai",
RequestType.CODE_DEBUG: "openai",
RequestType.CONCEPT_EXPLAIN: "gemini",
RequestType.CODE_REVIEW: "openai",
RequestType.TRANSLATION: "openai",
RequestType.OPTIMIZATION: "openai"
}
def select_provider(
self,
request: StructuredRequest,
available_providers: List[str]
) -> str:
"""Bira provider prema statičkim pravilima."""
preferred = self.rules.get(request.request_type)
# Ako preferirani provider nije dostupan, uzmi prvi dostupan
if preferred and preferred in available_providers:
return preferred
return available_providers[0] if available_providers else "openai"
class PerformanceRoutingStrategy(RoutingStrategy):
"""Routing baziran na performansama providera."""
def __init__(self, metric: str = "latency"):
"""
Inicijalizuje sa metrikom za poređenje.
Args:
metric: "latency", "success_rate", ili "cost"
"""
self.metric = metric
def select_provider(
self,
request: StructuredRequest,
available_providers: List[str]
) -> str:
"""Bira provider sa najboljim performansama."""
if not available_providers:
return "openai"
if len(available_providers) == 1:
return available_providers[0]
# Dobij performanse iz tracker-a
scores = {}
for provider in available_providers:
if self.metric == "latency":
# Manji je bolji
avg_latency = self._get_average_latency(provider)
scores[provider] = -avg_latency if avg_latency else 0
elif self.metric == "success_rate":
# Veći je bolji
scores[provider] = self._get_success_rate(provider)
else:
# Default skor
scores[provider] = 0
# Vrati provider sa najboljim skorom
return max(scores.items(), key=lambda x: x[1])[0]
def _get_average_latency(self, provider: str) -> float:
"""Dobija prosečnu latenciju providera."""
# Ovo bi trebalo da čita iz tracker-a
# Za sada vraćamo simulirane vrednosti
simulated = {
"openai": 1.5,
"gemini": 0.8
}
return simulated.get(provider, 2.0)
def _get_success_rate(self, provider: str) -> float:
"""Dobija stopu uspeha providera."""
simulated = {
"openai": 0.95,
"gemini": 0.92
}
return simulated.get(provider, 0.90)
class LoadBalancingStrategy(RoutingStrategy):
"""Round-robin load balancing između providera."""
def __init__(self):
self.last_selections = {}
self.provider_counts = {}
def select_provider(
self,
request: StructuredRequest,
available_providers: List[str]
) -> str:
"""Bira sledeći provider u round-robin redosledu."""
if not available_providers:
return "openai"
if len(available_providers) == 1:
return available_providers[0]
# Inicijalizuj brojače ako ne postoje
for provider in available_providers:
if provider not in self.provider_counts:
self.provider_counts[provider] = 0
# Nađi provider sa najmanje poziva
min_count = min(self.provider_counts.values())
candidates = [p for p in available_providers
if self.provider_counts.get(p, 0) == min_count]
# Izaberi random između kandidata sa istim brojem
selected = random.choice(candidates)
self.provider_counts[selected] += 1
return selected
class HybridRoutingStrategy(RoutingStrategy):
"""Kombinuje više strategija sa težinskim faktorima."""
def __init__(self):
self.static_strategy = StaticRoutingStrategy()
self.performance_strategy = PerformanceRoutingStrategy()
self.load_balance_strategy = LoadBalancingStrategy()
# Težinski faktori (mogu se konfigurisati)
self.weights = {
"static": 0.5,
"performance": 0.3,
"load_balance": 0.2
}
def select_provider(
self,
request: StructuredRequest,
available_providers: List[str]
) -> str:
"""Kombinuje strategije za finalni izbor."""
if not available_providers:
return "openai"
if len(available_providers) == 1:
return available_providers[0]
# Dobij preporuke od svake strategije
recommendations = {
"static": self.static_strategy.select_provider(request, available_providers),
"performance": self.performance_strategy.select_provider(request, available_providers),
"load_balance": self.load_balance_strategy.select_provider(request, available_providers)
}
# Računaj skorove za svakog providera
provider_scores = {}
for strategy, provider in recommendations.items():
weight = self.weights[strategy]
if provider not in provider_scores:
provider_scores[provider] = 0
provider_scores[provider] += weight
# Vrati provider sa najvećim skorom
return max(provider_scores.items(), key=lambda x: x[1])[0]
class SmartProviderRouter:
"""Glavni router koji upravlja svim strategijama."""
def __init__(self, strategy: Optional[RoutingStrategy] = None):
"""
Inicijalizuje router.
Args:
strategy: Routing strategija (default: HybridRoutingStrategy)
"""
self.strategy = strategy or HybridRoutingStrategy()
self.routing_history = []
def get_available_providers(self) -> List[str]:
"""Vraća listu trenutno dostupnih providera."""
available = []
# Proveri koji provideri imaju API ključeve
if Config.OPENAI_API_KEY:
# Proveri circuit breaker status
cb_name = "ai_openai"
if cb_name in circuit_registry:
cb = circuit_registry[cb_name]
if cb.state.value != "open":
available.append("openai")
else:
available.append("openai")
if Config.GEMINI_API_KEY:
cb_name = "ai_gemini"
if cb_name in circuit_registry:
cb = circuit_registry[cb_name]
if cb.state.value != "open":
available.append("gemini")
else:
available.append("gemini")
return available
def route_request(
self,
request: StructuredRequest,
override_provider: Optional[str] = None
) -> Tuple[str, Dict[str, Any]]:
"""
Rutira zahtev ka odgovarajućem provideru.
Args:
request: Strukturiran zahtev
override_provider: Opciono forsiraj provider
Returns:
Tuple (provider_name, routing_metadata)
"""
# Ako je provider eksplicitno zadat
if override_provider:
metadata = {
"strategy": "override",
"reason": "Explicitly requested",
"timestamp": datetime.now().isoformat()
}
self._record_routing(request, override_provider, metadata)
return override_provider, metadata
# Dobij dostupne providere
available = self.get_available_providers()
if not available:
# Nema dostupnih providera
metadata = {
"strategy": "fallback",
"reason": "No available providers",
"timestamp": datetime.now().isoformat()
}
self._record_routing(request, "simulation", metadata)
return "simulation", metadata
# Koristi strategiju za izbor
selected = self.strategy.select_provider(request, available)
# Generiši metadata
metadata = {
"strategy": type(self.strategy).__name__,
"available_providers": available,
"request_type": request.request_type.value,
"selected_reason": self._get_selection_reason(request, selected),
"timestamp": datetime.now().isoformat()
}
# Zapamti routing odluku
self._record_routing(request, selected, metadata)
return selected, metadata
def _get_selection_reason(
self,
request: StructuredRequest,
selected: str
) -> str:
"""Generiše razlog izbora providera."""
if isinstance(self.strategy, StaticRoutingStrategy):
return f"Best for {request.request_type.value} requests"
elif isinstance(self.strategy, PerformanceRoutingStrategy):
return f"Best performance on {self.strategy.metric}"
elif isinstance(self.strategy, LoadBalancingStrategy):
return "Load balancing distribution"
else:
return "Hybrid strategy decision"
def _record_routing(
self,
request: StructuredRequest,
provider: str,
metadata: Dict[str, Any]
):
"""Beleži routing odluku za analizu."""
self.routing_history.append({
"timestamp": datetime.now(),
"request_type": request.request_type.value,
"provider": provider,
"metadata": metadata
})
# Ograniči istoriju na poslednjih 1000 zapisa
if len(self.routing_history) > 1000:
self.routing_history = self.routing_history[-1000:]
def get_routing_statistics(self) -> Dict[str, Any]:
"""Vraća statistiku rutiranja."""
if not self.routing_history:
return {"message": "No routing history"}
# Analiza po provideru
provider_counts = {}
type_counts = {}
for record in self.routing_history:
provider = record["provider"]
req_type = record["request_type"]
provider_counts[provider] = provider_counts.get(provider, 0) + 1
type_counts[req_type] = type_counts.get(req_type, 0) + 1
# Analiza po vremenu (poslednji sat)
recent_records = [
r for r in self.routing_history
if r["timestamp"] > datetime.now() - timedelta(hours=1)
]
return {
"total_requests": len(self.routing_history),
"providers": provider_counts,
"request_types": type_counts,
"recent_hour_count": len(recent_records),
"strategy": type(self.strategy).__name__
}
# Globalni router
smart_router = SmartProviderRouter()
# Test funkcionalnost
if __name__ == "__main__":
print("🧪 Test Provider Router-a")
print("=" * 50)
from web_api.models.request_types import RequestAnalyzer
# Test zahtevi
test_requests = [
"Napiši Python funkciju za sortiranje",
"Objasni mi šta su closure",
"Zdravo, kako si danas?",
"Debug ovaj kod koji baca error",
"Optimizuj ovu petlju"
]
# Testiraj različite strategije
strategies = [
("Static", StaticRoutingStrategy()),
("Performance", PerformanceRoutingStrategy()),
("LoadBalance", LoadBalancingStrategy()),
("Hybrid", HybridRoutingStrategy())
]
for strategy_name, strategy in strategies:
print(f"\n📋 Testiranje {strategy_name} strategije:")
router = SmartProviderRouter(strategy)
for req_text in test_requests[:3]: # Samo prva 3 za kratak test
# Kreiraj strukturiran zahtev
structured = RequestAnalyzer.create_structured_request(req_text)
# Rutiraj
provider, metadata = router.route_request(structured)
print(f"\n Zahtev: '{req_text[:40]}...'")
print(f" Tip: {structured.request_type.value}")
print(f" Provider: {provider}")
print(f" Razlog: {metadata.get('selected_reason', 'N/A')}")
# Prikaži statistiku
print("\n📊 Routing statistika:")
stats = router.get_routing_statistics()
print(f" Ukupno zahteva: {stats['total_requests']}")
print(f" Po providerima: {stats['providers']}")
💡 PRO TIP: Umesto čuvanja routing istorije u memoriji, razmisli o event streaming. Svaku routing odluku možeš da publiguješ kao event koji mogu da konzumiraju analytics servisi, monitoring, ili machine learning pipeline za optimizaciju strategije.
⚡ SAVET ZA OPTIMIZACIJU: Čuvanje routing_history u listi u memoriji je jednostavno, ali nije skalabilno. U produkcionom sistemu, ova lista bi brzo porasla i potrošila svu memoriju servera. Bolje rešenje je emitovanje događaja (events). Svaka odluka rutera bi se slala kao poruka u message queue sistem (kao RabbitMQ) ili event stream (kao Google Cloud Pub/Sub). Ovo odvaja logovanje od glavne logike, ne utiče na performanse API-ja i omogućava asinhronu obradu i analizu podataka u realnom vremenu.
Dodatni primeri i zabavne vežbe
💡 PRO TIP: Nauči Vasu da “pita nazad”. Ako je confidence_score < 0.4, vrati korisniku: “Nisam siguran da li želiš debug ili explain. Možeš li da preciziraš?”. Ovo drastično smanjuje pogrešna rutiranja.
🎯 ALTERNATIVNO REŠENJE: Umesto enum-a, možeš koristiti Python dataclass plus Literal tipove iz typing. Dobijаš iste benefite uz manje bojlerplejtа, a mypy će ti hvatati greške u vrednostima tipa već u vreme kompajlacije.
🎈 ZABAVNA ČINJENICA: Round-robin load-balancer u Linux IPVS-u postoji od 1998. i dalje napaja milione konekcija u Kubernetes-u – dokaz da jednostavne strategije ponekad prežive sve tehnologije.
🤔 MINI-KVIZ:
- Koja dva uslova moraju biti ispunjena da bi
LoadBalancingStrategyizabrala random.choice između kandidata? - Šta se dešava sa istorijom rutiranja kada premaši 1000 zapisa?
🔄 VEŽBA: Proširi HybridRoutingStrategy tako da uzme u obzir i cost per-token. Potrebno je da simuliraš cenu u PerformanceRoutingStrategy._get_average_cost() i dodaćeš težinu 0.2 u self.weights.
Integracija sa FastAPI endpoint-ima
Sada ćemo integrisati strukturirane zahteve i routing u postojeće API endpoint-e.
📚 NIVO 1 – Osnovno objašnjenje
Integrisaćemo novi sistem tako da:
/pitajendpoint prepoznaje tip pitanja- Automatski bira najbolji AI servis
- Prikazuje korisniku koji servis je korišćen i zašto
- Omogućava override izbora ako korisnik želi
🚀 NIVO 2 – Dublje razumevanje
Endpoint treba da:
- Prima i običan string i strukturiran zahtev
- Vraća ne samo odgovor već i metadata o rutiranju
- Omogućava fine-tuning kroz query parametre
- Loguje sve za kasniju analizu
🔍 UVID: Kreiranjem SmartProviderRouter-a, mi zapravo implementiramo “Strategy” i “Facade” dizajn paterne:
- Strategy Pattern:
RoutingStrategyje interfejs, aStaticRoutingStrategy,PerformanceRoutingStrategyitd. su konkretne implementacije. Ovo nam omogućava da menjamo algoritam rutiranja u letu bez izmene glavne logike routera. - Facade Pattern:
SmartProviderRoutersluži kao jedinstvena, pojednostavljena tačka interakcije (route_requestmetoda) koja skriva svu kompleksnost provere dostupnosti, izbora strategije i beleženja odluka. Ovo je ključno za održavanje čistog i skalabilnog koda.
📊 DIJAGRAM: Životni ciklus /pitaj zahteva u FastAPI
[FastAPI App]
│
Incoming HTTP POST -> │ @app.post("/pitaj")
(JSON Body) │
├─ 1. Validacija: Da li postoji polje "pitanje"?
│
├─ 2. Analiza: RequestAnalyzer.create_structured_request(pitanje)
│ │
│ └─> [StructuredRequest objekat]
│
├─ 3. Rutiranje: smart_router.route_request(structured_request)
│ │
│ └─> ("provider_name", {metadata})
│
├─ 4. Factory Reset (ako treba): AIServiceFactory.reset()
│
├─ 5. Poziv Servisa: ai_service.pozovi_ai(enhanced_prompt)
│ │
│ └─> [Odgovor od AI modela]
│
├─ 6. Formatiranje Odgovora: Kreiranje finalnog JSON-a
│
Outgoing HTTP 200 <─- │ return response
(JSON Body) │
Izmeni fajl: src/web_api/app.py:
"""
FastAPI aplikacija za Učitelja Vasu
Transformiše konzolnu aplikaciju u web servis
"""
from fastapi import FastAPI, HTTPException
from fastapi.middleware.cors import CORSMiddleware
from typing import Dict, Any, Optional, Union
from datetime import datetime
import sys
import os
# Dodaj src folder u Python path
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
# Import Vasa modula
from vasa_core import pozdrav, predstavi_se, VASA_LICNOST
from ai_services.ai_factory import AIServiceFactory
from utils.config import Config
from utils.performance_tracker import tracker
# Import za routing i request handling
from web_api.models.request_types import RequestAnalyzer, RequestType, StructuredRequest
from web_api.models.router import smart_router
# Kreiraj FastAPI instancu
app = FastAPI(
title="Učitelj Vasa API",
description="AI asistent za učenje programiranja",
version="1.0.0"
)
# Konfiguriši CORS
app.add_middleware(
CORSMiddleware,
allow_origins=["*"], # U produkciji, navedi specifične domene
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# Globalne varijable
ai_service = None
startup_time = None
@app.on_event("startup")
async def startup_event():
"""Inicijalizuje AI servis pri pokretanju."""
global ai_service, startup_time
startup_time = datetime.now()
print("🚀 Pokrećem Učitelja Vasu Web API...")
try:
ai_service = AIServiceFactory.create_resilient_service()
print("✅ AI servis spreman!")
except Exception as e:
print(f"⚠️ Problem sa AI servisom: {e}")
print("📌 API će raditi u ograničenom režimu")
@app.get("/")
async def root():
"""Osnovne informacije o API-ju."""
return {
"ime": "Učitelj Vasa API",
"verzija": "1.0.0",
"status": "aktivan",
"opis": "AI asistent za učenje programiranja"
}
@app.get("/health")
async def health_check():
"""Osnovni health check endpoint."""
return {
"status": "healthy",
"service": "ucitelj-vasa-api",
"timestamp": datetime.now().isoformat()
}
@app.get("/health/ai")
async def ai_service_health():
"""Proverava health AI servisa."""
health_info = {
"service_exists": ai_service is not None,
"provider": Config.AI_PROVIDER,
"timestamp": datetime.now().isoformat()
}
if ai_service:
try:
# Pokušaj da dobiješ postavke kao brzu proveru
settings = ai_service.get_current_settings()
health_info["responsive"] = True
health_info["model"] = settings.get("model", "unknown")
except:
health_info["responsive"] = False
else:
health_info["responsive"] = False
# Određi overall status
if health_info["service_exists"] and health_info["responsive"]:
health_info["status"] = "healthy"
elif health_info["service_exists"]:
health_info["status"] = "degraded"
else:
health_info["status"] = "unavailable"
return health_info
@app.get("/zdravo")
async def zdravo():
"""Vasa pozdravlja."""
return {
"poruka": pozdrav(),
"tip": "pozdrav"
}
@app.get("/o-vasi")
async def o_vasi():
"""Informacije o Učitelju Vasi."""
return {
"ime": "Učitelj Vasa",
"opis": predstavi_se(),
"mogucnosti": [
"Odgovara na pitanja o programiranju",
"Objašnjava koncepte",
"Pomaže sa debug-ovanjem",
"Daje primere koda"
]
}
@app.post("/pitaj",
summary="Postavi pitanje sa inteligentnim rutiranjem",
description="""
Napredni endpoint koji:
- Analizira tip pitanja
- Bira najbolji AI provider
- Optimizuje parametre
- Vraća detaljan odgovor sa metapodacima
Primer strukturiranog zahteva:
```json
{
"pitanje": "Napiši funkciju koja sortira listu",
"tip": "code",
"context": {
"programming_language": "python",
"user_level": "beginner"
}
}
```
""",
response_description="Odgovor sa routing informacijama"
)
async def pitaj_vasu(
pitanje_data: Union[Dict[str, str], Dict[str, Any]],
force_provider: Optional[str] = None,
analyze_request: bool = True
):
"""
Postavlja pitanje Učitelju Vasi sa inteligentnim rutiranjem.
Podržava:
- Jednostavan format: {"pitanje": "..."}
- Strukturiran format: {"pitanje": "...", "tip": "code", "context": {...}}
Query parametri:
- force_provider: Forsiraj specifičan provider (openai/gemini)
- analyze_request: Da li da analizira i struktuira zahtev (default: true)
"""
# Osnovna validacija
if "pitanje" not in pitanje_data:
raise HTTPException(
status_code=400,
detail="Nedostaje 'pitanje' polje u zahtevu"
)
pitanje = pitanje_data["pitanje"]
if not pitanje.strip():
raise HTTPException(
status_code=400,
detail="Pitanje ne može biti prazno"
)
# Kreiraj strukturiran zahtev
if analyze_request:
# Proveri da li je već strukturiran
if "tip" in pitanje_data:
# Korisnik je poslao strukturiran zahtev
try:
request_type = RequestType(pitanje_data["tip"])
except ValueError:
request_type = RequestType.CHAT
structured_request = StructuredRequest(
content=pitanje,
request_type=request_type,
context=pitanje_data.get("context", {}),
preferences=pitanje_data.get("preferences", {})
)
else:
# Analiziraj sirovo pitanje
structured_request = RequestAnalyzer.create_structured_request(
pitanje,
additional_context=pitanje_data.get("context")
)
else:
# Bez analize, tretiraj kao obican chat
structured_request = StructuredRequest(
content=pitanje,
request_type=RequestType.CHAT
)
# Rutiraj zahtev
selected_provider, routing_metadata = smart_router.route_request(
structured_request,
override_provider=force_provider
)
# Proveri da li imamo AI servis
if not ai_service:
return {
"greska": "AI servis trenutno nije dostupan",
"tip_zahteva": structured_request.request_type.value,
"routing": routing_metadata
}
try:
# Promeni provider ako je potrebno
original_provider = Config.AI_PROVIDER
if selected_provider != original_provider and selected_provider != "simulation":
Config.AI_PROVIDER = selected_provider
# Reset factory da učita novi provider
from ai_services.ai_factory import AIServiceFactory
AIServiceFactory.reset()
current_service = AIServiceFactory.create_resilient_service()
else:
current_service = ai_service
# Dobij optimizovane parametre
optimized_params = structured_request.get_optimized_params()
# Primeni parametre
current_service.apply_settings(optimized_params)
# Generiši poboljšan prompt
enhanced_prompt = structured_request.get_enhanced_prompt()
# Pozovi AI sa personalizacijom ako postoji
if hasattr(current_service, 'pozovi_ai_personalizovano'):
# Ovde bi trebalo proslediti user profile
odgovor = current_service.pozovi_ai(enhanced_prompt)
else:
odgovor = current_service.pozovi_ai(enhanced_prompt)
# Vrati originalni provider
if selected_provider != original_provider:
Config.AI_PROVIDER = original_provider
AIServiceFactory.reset()
# Pripremi response
response = {
"pitanje": pitanje,
"odgovor": odgovor,
"tip_zahteva": structured_request.request_type.value,
"provider": {
"selected": selected_provider,
"reason": routing_metadata.get("selected_reason"),
"strategy": routing_metadata.get("strategy")
},
"optimizacija": {
"temperature": optimized_params.get("temperature"),
"max_tokens": optimized_params.get("max_tokens")
}
}
# Dodaj kontekst ako postoji
if structured_request.context.has_code_context():
response["context"] = {
"language": structured_request.context.programming_language,
"has_code": bool(structured_request.context.code_snippet),
"has_error": bool(structured_request.context.error_message)
}
return response
except Exception as e:
# Loguj grešku ali vrati user-friendly poruku
print(f"❌ Greška pri obradi pitanja: {e}")
return {
"greska": "Dogodila se greška pri obradi pitanja",
"savet": "Pokušaj ponovo ili promeni formulaciju pitanja",
"tip_zahteva": structured_request.request_type.value,
"provider_pokušan": selected_provider
}
@app.get("/providers")
async def get_providers():
"""Vraća informacije o dostupnim AI providerima."""
providers = []
# Proveri OpenAI
if Config.OPENAI_API_KEY:
providers.append({
"name": "openai",
"display_name": "OpenAI GPT",
"available": True,
"is_active": Config.AI_PROVIDER == "openai",
"features": ["chat", "code_generation", "analysis"]
})
# Proveri Gemini
if Config.GEMINI_API_KEY:
providers.append({
"name": "gemini",
"display_name": "Google Gemini",
"available": True,
"is_active": Config.AI_PROVIDER == "gemini",
"features": ["chat", "multimodal", "fast_responses"]
})
# Ako nijedan nije dostupan
if not providers:
providers.append({
"name": "simulation",
"display_name": "Lokalna simulacija",
"available": True,
"is_active": True,
"features": ["basic_responses"]
})
return {
"providers": providers,
"active_provider": Config.AI_PROVIDER,
"total_configured": len([p for p in providers if p["name"] != "simulation"])
}
@app.get("/status")
async def get_status():
"""Vraća osnovni status sistema."""
# Računaj uptime
uptime_seconds = 0
if startup_time:
uptime_seconds = (datetime.now() - startup_time).total_seconds()
# Osnovno stanje
status = {
"status": "operational" if ai_service else "limited",
"uptime_seconds": int(uptime_seconds),
"uptime_human": f"{int(uptime_seconds // 60)} minuta",
"current_provider": Config.AI_PROVIDER,
"api_version": "1.0.0",
"timestamp": datetime.now().isoformat()
}
# Broj dostupnih providera
available_count = 0
if Config.OPENAI_API_KEY:
available_count += 1
if Config.GEMINI_API_KEY:
available_count += 1
status["providers_available"] = available_count
status["multi_provider_enabled"] = available_count > 1
# Circuit breaker status ako postoji
if ai_service and hasattr(ai_service, '_circuit_breaker_call'):
try:
cb = ai_service._circuit_breaker_call.circuit_breaker
status["circuit_breaker"] = cb.state.value
except:
status["circuit_breaker"] = "unknown"
return status
@app.get("/providers/current")
async def get_current_provider():
"""Vraća detalje o trenutno aktivnom provideru."""
current = Config.AI_PROVIDER
info = {
"provider": current,
"display_name": "OpenAI GPT" if current == "openai" else "Google Gemini",
"active_since": startup_time.isoformat() if startup_time else None
}
# Dodaj informacije o servisu ako postoji
if ai_service:
try:
settings = ai_service.get_current_settings()
info["model"] = settings.get("model", "nepoznat")
info["service_status"] = "operational"
except:
info["service_status"] = "degraded"
else:
info["service_status"] = "unavailable"
return info
@app.get("/providers/statistics")
async def get_provider_statistics():
"""Vraća osnovne statistike o korišćenju providera."""
if not hasattr(tracker, 'all_metrics') or not tracker.all_metrics:
return {
"message": "Nema dovoljno podataka",
"total_requests": 0,
"providers": {}
}
# Grupiši podatke po providerima
stats = {}
for metric in tracker.all_metrics:
# Koristi dictionary pristup umesto atributa
provider = metric.get('provider', 'unknown')
if provider not in stats:
stats[provider] = {
"total_requests": 0,
"successful_requests": 0,
"failed_requests": 0,
"total_tokens": 0
}
stats[provider]["total_requests"] += 1
# Koristi get() metod za bezbedno čitanje
if metric.get('success', False):
stats[provider]["successful_requests"] += 1
stats[provider]["total_tokens"] += metric.get('tokens_used', 0)
else:
stats[provider]["failed_requests"] += 1
# Dodaj procente
for provider, data in stats.items():
if data["total_requests"] > 0:
data["success_rate"] = round(
(data["successful_requests"] / data["total_requests"]) * 100,
2
)
else:
data["success_rate"] = 0
return {
"total_requests": len(tracker.all_metrics),
"providers": stats,
"collection_started": tracker.all_metrics[0].get('timestamp') if tracker.all_metrics else None
}
@app.get("/request-types")
async def get_request_types():
"""Vraća sve podržane tipove zahteva sa opisima."""
types = []
for req_type in RequestType:
types.append({
"type": req_type.value,
"description": req_type.get_description(),
"preferred_provider": req_type.get_preferred_provider()
})
return {
"supported_types": types,
"total": len(types)
}
@app.get("/routing/stats")
async def get_routing_statistics():
"""Vraća statistiku routing odluka."""
stats = smart_router.get_routing_statistics()
# Dodaj trenutnu strategiju
stats["current_strategy"] = type(smart_router.strategy).__name__
# Dodaj dostupne providere
stats["available_providers"] = smart_router.get_available_providers()
return stats
@app.post("/routing/strategy")
async def change_routing_strategy(strategy_name: str):
"""
Menja routing strategiju.
Dostupne strategije:
- static: Fiksna pravila po tipu
- performance: Bazirana na performansama
- loadbalance: Round-robin
- hybrid: Kombinacija (default)
"""
from web_api.models.router import (
StaticRoutingStrategy,
PerformanceRoutingStrategy,
LoadBalancingStrategy,
HybridRoutingStrategy
)
strategies = {
"static": StaticRoutingStrategy,
"performance": PerformanceRoutingStrategy,
"loadbalance": LoadBalancingStrategy,
"hybrid": HybridRoutingStrategy
}
if strategy_name not in strategies:
raise HTTPException(
status_code=400,
detail=f"Nepoznata strategija. Dostupne: {list(strategies.keys())}"
)
# Promeni strategiju
smart_router.strategy = strategies[strategy_name]()
return {
"message": f"Routing strategija promenjena na: {strategy_name}",
"strategy": strategy_name,
"description": smart_router._get_selection_reason(None, "")
}
# Primer strukturiranog zahteva za dokumentaciju
structured_request_example = {
"pitanje": "Napiši funkciju koja sortira listu",
"tip": "code",
"context": {
"programming_language": "python",
"user_level": "beginner"
},
"preferences": {
"temperature": 0.5,
"max_tokens": 200
}
}
if __name__ == "__main__":
# Za development - pokreni server direktno
import uvicorn
print("🚀 Pokrećem Učitelja Vasu API na http://localhost:8000")
print("📚 Dokumentacija dostupna na http://localhost:8000/docs")
uvicorn.run(
"app:app",
host="0.0.0.0",
port=8000,
reload=True # Automatski restart pri promeni koda
)
Praktična implementacija
Testiranje routing sistema
Kreiraj src/test_routing.py:
"""
Test scenariji za request routing sistem
"""
import requests
import json
from typing import Dict, Any
BASE_URL = "http://localhost:8000"
def test_request_types():
"""Testira prepoznavanje tipova zahteva."""
print("\n🧪 TEST 1: Prepoznavanje tipova zahteva")
print("=" * 50)
test_cases = [
{
"pitanje": "Napiši funkciju za sortiranje liste u Python-u",
"expected_type": "code"
},
{
"pitanje": "Zašto mi ovaj kod baca IndexError?",
"expected_type": "debug"
},
{
"pitanje": "Objasni mi šta je rekurzija",
"expected_type": "explain"
},
{
"pitanje": "Zdravo, kako si?",
"expected_type": "chat"
}
]
for test in test_cases:
response = requests.post(
f"{BASE_URL}/pitaj",
json=test
)
if response.status_code == 200:
data = response.json()
detected_type = data.get("tip_zahteva", "unknown")
print(f"\nPitanje: '{test['pitanje'][:50]}...'")
print(f"Očekivan tip: {test['expected_type']}")
print(f"Detektovan tip: {detected_type}")
print(f"Provider: {data.get('provider', {}).get('selected', 'N/A')}")
if detected_type == test["expected_type"]:
print("✅ Tačno prepoznat tip!")
else:
print("❌ Pogrešan tip")
else:
print(f"❌ Greška: {response.status_code}")
def test_structured_requests():
"""Testira strukturirane zahteve."""
print("\n🧪 TEST 2: Strukturirani zahtevi")
print("=" * 50)
structured = {
"pitanje": "Implementiraj bubble sort algoritam",
"tip": "code",
"context": {
"programming_language": "python",
"user_level": "beginner"
},
"preferences": {
"temperature": 0.3,
"max_tokens": 300
}
}
response = requests.post(f"{BASE_URL}/pitaj", json=structured)
if response.status_code == 200:
data = response.json()
print(f"\nStrukturiran zahtev poslat")
print(f"Tip: {data.get('tip_zahteva')}")
print(f"Provider: {data.get('provider', {}).get('selected')}")
print(f"Razlog: {data.get('provider', {}).get('reason')}")
print(f"Optimizacija: {data.get('optimizacija')}")
if "context" in data:
print(f"Kontekst prepoznat: {data['context']}")
else:
print(f"❌ Greška: {response.status_code}")
print(response.json())
def test_provider_override():
"""Testira override providera."""
print("\n🧪 TEST 3: Override providera")
print("=" * 50)
# Test sa forsiranim providerom
response = requests.post(
f"{BASE_URL}/pitaj",
json={"pitanje": "Šta je Python?"},
params={"force_provider": "gemini"}
)
if response.status_code == 200:
data = response.json()
selected = data.get("provider", {}).get("selected")
strategy = data.get("provider", {}).get("strategy")
print(f"\nForsiran provider: gemini")
print(f"Korišćen provider: {selected}")
print(f"Strategija: {strategy}")
if selected == "gemini" and strategy == "override":
print("✅ Override radi!")
else:
print("❌ Override ne radi")
def test_routing_strategies():
"""Testira različite routing strategije."""
print("\n🧪 TEST 4: Routing strategije")
print("=" * 50)
strategies = ["static", "performance", "loadbalance", "hybrid"]
for strategy in strategies:
# Promeni strategiju
response = requests.post(
f"{BASE_URL}/routing/strategy",
params={"strategy_name": strategy}
)
if response.status_code == 200:
print(f"\n📋 Strategija: {strategy}")
# Testiraj sa istim pitanjem
test_response = requests.post(
f"{BASE_URL}/pitaj",
json={"pitanje": "Napiši hello world program"}
)
if test_response.status_code == 200:
data = test_response.json()
provider = data.get("provider", {}).get("selected")
reason = data.get("provider", {}).get("reason")
print(f" Provider: {provider}")
print(f" Razlog: {reason}")
def test_request_types_endpoint():
"""Testira endpoint za tipove zahteva."""
print("\n🧪 TEST 5: Request types endpoint")
print("=" * 50)
response = requests.get(f"{BASE_URL}/request-types")
if response.status_code == 200:
data = response.json()
print(f"\nPodržano tipova: {data['total']}")
print("\nTipovi:")
for req_type in data["supported_types"]:
print(f"\n {req_type['type']}:")
print(f" Opis: {req_type['description']}")
print(f" Preferirani: {req_type['preferred_provider']}")
def test_routing_statistics():
"""Testira routing statistiku."""
print("\n🧪 TEST 6: Routing statistika")
print("=" * 50)
# Prvo generiši neke zahteve
test_questions = [
"Kako da sortiram listu?",
"Napiši funkciju za fibonacci",
"Debug ovaj kod",
"Objasni closure",
"Zdravo!"
]
for q in test_questions:
requests.post(f"{BASE_URL}/pitaj", json={"pitanje": q})
# Dobij statistiku
response = requests.get(f"{BASE_URL}/routing/stats")
if response.status_code == 200:
stats = response.json()
print(f"\nUkupno zahteva: {stats.get('total_requests', 0)}")
print(f"Trenutna strategija: {stats.get('current_strategy')}")
print(f"Dostupni provideri: {stats.get('available_providers')}")
if "providers" in stats:
print("\nPo providerima:")
for provider, count in stats["providers"].items():
print(f" {provider}: {count}")
if "request_types" in stats:
print("\nPo tipovima:")
for req_type, count in stats["request_types"].items():
print(f" {req_type}: {count}")
if __name__ == "__main__":
print("🚀 ROUTING SYSTEM TEST SUITE")
print("=" * 60)
print("\n⚠️ Proveri da li je server pokrenut na http://localhost:8000")
input("Pritisni ENTER za početak testiranja...")
try:
# Proveri da li server radi
health = requests.get(f"{BASE_URL}/health")
if health.status_code != 200:
print("❌ Server ne odgovara!")
exit(1)
# Pokreni testove
test_request_types()
test_structured_requests()
test_provider_override()
test_routing_strategies()
test_request_types_endpoint()
test_routing_statistics()
print("\n✅ Svi testovi završeni!")
except requests.exceptions.ConnectionError:
print("❌ Ne mogu da se povežem sa serverom!")
print(" Pokreni server sa: python src/web_api/run_server.py")
except Exception as e:
print(f"❌ Neočekivana greška: {e}")
🔄 VEŽBA: Kreiraj custom routing strategiju koja:
- Preferira OpenAI ujutru (pre 12h) za “svežinu”
- Preferira Gemini popodne za “brzinu”
- Vikendom uvek koristi jeftiniji servis
🔄 POKUŠAJ SAM:
- Pronađi u test-fajlu
test_routing.pyscenario koji treće pitanje “Debug ovaj kod koji baca error” šalje statičkom strategijom. Prepiši ga tako da:- Dinamički odabere najbržeg providera (metrika latency),
- Sačuva povratni
routing_metadatau JSON fajllatest_route.json.
- Pokreni test i proveri da li se fajl kreira sa ispravnim podacima.
Česte greške i rešenja
❌ GREŠKA: RequestAnalyzer pogrešno kategoriše zahteve
💡 REŠENJE:
- Dodaj više ključnih reči u TYPE_KEYWORDS
- Povećaj skorove za specifične obrasce
- Razmisli o ML-based klasifikatoru za produkciju
🔬 DETALJNIJE: Sistemski problem sa pristupom zasnovanom na ključnim rečima (TYPE_KEYWORDS) je njegova krhkost (brittleness). On ne razume kontekst, već samo prepoznaje niske karaktera. Na primer, rečenica “Objasni grešku u mom kodu” sadrži i “objasni” (CONCEPT_EXPLAIN) i “grešku” (CODE_DEBUG). Naš trenutni sistem ponderisanja će možda uspeti, ali se lako može prevariti. Problem je u tome što je jezik dvosmislen, a naš sistem je rigidan. Veliki jezički modeli (LLM) su fundamentalno drugačiji jer su trenirani da razumeju semantičke veze i nameru iza reči, a ne samo reči same po sebi. Naš RequestAnalyzer je deterministički sistem; LLM je probabilistički. To je ključna razlika koja objašnjava zašto je “pogrešna kategorizacija” sistemski rizik kod ovog pristupa.
❌ GREŠKA: Routing uvek bira isti provider
💡 REŠENJE:
- Proveri da li imaš oba API ključa konfigurisana
- Možda je circuit breaker otvoren za jedan provider
- Debug sa
get_available_providers()da vidiš šta je dostupno
❌ GREŠKA: Override provider ne radi
💡 REŠENJE:
- Proveri da li prosleđuješ kao query parametar, ne u body
- Provider ime mora biti tačno (“openai”, ne “OpenAI”)
- Možda pokušavaš da forsiraš provider koji nije dostupan
❌ GREŠKA: Optimizovani parametri se ne primenjuju
💡 REŠENJE:
- Proveri da li AI servis implementira
apply_settings() - Možda resilience wrapper ne prosleđuje poziv
- Loguj parametre pre i posle primene
❌ GREŠKA: Memory leak sa routing istorijom
💡 REŠENJE:
- Implementiraj automatsko čišćenje starijih od N sati
- Koristi deque sa maxlen umesto liste
- Razmisli o eksportovanju u fajl i čišćenju memorije
Proveri svoje razumevanje
[NIVO 1]:
- Koji su osnovni tipovi zahteva koje Vasa prepoznaje?
- Šta je provider routing i zašto je koristan?
- Kako strukturiran zahtev pomaže u boljem odgovoru?
- Šta znači “override” provider opcija?
[NIVO 2]:
- Koje su prednosti TYPE_KEYWORDS pristupa i koja su ograničenja?
- Kako bi implementirao A/B testing kroz routing?
- Šta je hybrid routing strategija i kada je korisna?
- Kako bi dodao podršku za novi tip zahteva?
- Koje metrike bi pratio za optimizaciju routinga?
🤔 MINI-KVIZ:
- Koja routing strategija bi bila najbolja za produkciju sa 10000+ zahteva dnevno?
- Kako bi implementirao “sticky sessions” – da korisnik uvek ide na isti provider tokom sesije?
- Šta bi se desilo ako svi provideri budu nedostupni?
Ažuriranje dokumentacije
Ažuriraj README.md – dodaj ovu sekciju posle postojećih sekcija:
## 🚀 Trenutni Status
- ✅ Dan -3: Python 3.13+ instaliran
- ✅ Dan -2: PyCharm unified edition podešen
- ✅ Dan -1: GitHub repository kreiran
- ✅ Dan 0: Profesionalna struktura projekta
- ✅ Dan 1: Prvi Python moduli - Vasa može da pozdravi!
- ✅ Dan 2: Razumevanje AI API-ja - simulacija komunikacije
- ✅ Dan 3: Multi-provider podrška - OpenAI i Gemini
- ✅ Dan 4: Prvi AI poziv - univerzalni sistem sa SSL fix-om
- ✅ Dan 5: Profilisanje i optimizacija - automatski izbor najboljih postavki
- ✅ Dan 6: Resilience sistem - retry, circuit breaker, fallback i graceful degradation
- ✅ Dan 7: Napredna personalizacija - profili, preference i adaptivno učenje
- ✅ Dan 8: Uvod u FastAPI - Učitelj Vasa je sada web servis!
- ✅ Dan 9: Multi-provider web endpoint-i - transparentnost i monitoring
- ✅ Dan 10: Strukturirani zahtevi i inteligentno rutiranje! 🎯
- ⏳ Dan 11: Validacija sa Pydantic i provider-specific modeli (sutra)
## 🎯 Request Routing
### Tipovi zahteva:
- `chat` - Obična konverzacija
- `code` - Generisanje koda
- `debug` - Pomoć sa greškama
- `explain` - Objašnjenje koncepata
- `review` - Pregled koda
- `translate` - Prevod između jezika
- `optimize` - Optimizacija koda
### Routing strategije:
- **Static** - Fiksna pravila po tipu
- **Performance** - Bazirana na metrikama
- **LoadBalance** - Round-robin distribucija
- **Hybrid** - Kombinacija strategija
### API Endpoints:
- `POST /pitaj` - Napredni endpoint sa rutiranjem
- `GET /request-types` - Lista podržanih tipova
- `GET /routing/stats` - Statistika rutiranja
- `POST /routing/strategy` - Promena strategije
Dodaj u docs/development_log.md novu sekciju:
## Dan 10: Struktura zahteva i provider routing (20.06.2025)
### Šta je urađeno:
- ✅ Definisani tipovi zahteva (RequestType enum)
- ✅ RequestAnalyzer za prepoznavanje tipova
- ✅ StructuredRequest sa kontekstom i preferencama
- ✅ Četiri routing strategije (static, performance, load balance, hybrid)
- ✅ SmartProviderRouter sa istorijom odluka
- ✅ Integracija u /pitaj endpoint
- ✅ Novi API endpoint-i za upravljanje
- ✅ Test suite za sve komponente
### Naučene lekcije:
- Strukturirani zahtevi omogućavaju bolju optimizaciju
- Različiti tipovi zahteva zahtevaju različite pristupe
- Routing strategije mogu drastično poboljšati performanse
- Hybrid pristup daje najbolju fleksibilnost
- Metadata o rutiranju korisna za debugging
### Problemi i rešenja:
- **Problem**: Kako tačno prepoznati tip zahteva?
- **Rešenje**: Kombinacija ključnih reči i pattern matching
- **Problem**: Kada koristiti koji provider?
- **Rešenje**: Empirijski podaci + mogućnost override
### Testiranje:
- Request analyzer: 80%+ tačnost prepoznavanja
- Routing strategije rade kako je očekivano
- Override funkcionalnost testirana
- Statistike se pravilno beleže
### Za sutra (Dan 11):
- Validacija sa Pydantic modelima
- Provider-specific request/response modeli
- Naprednije error handling
Git commit za danas
git add .
git commit -m "Dan 10: Implementiran sistem strukturiranih zahteva sa inteligentnim provider rutiranjem!"
git push
ČESTITAM! 🎉 Učitelj Vasa sada ima sofisticirani sistem rutiranja! Implementirao si:
- Prepoznavanje 7 različitih tipova zahteva
- 4 različite routing strategije
- Kontekstualno svesno procesiranje
- Transparentno praćenje svih odluka
Ovo je ključni korak ka pravom production-ready AI sistemu!
Sutra Učitelj Vasa uči
Sutra ćemo se fokusirati na validaciju podataka sa Pydantic bibliotekom. Naučićeš kako da kreiraš type-safe modele za request i response, implementiraš automatsku validaciju svih podataka, i kreiraš provider-specific modele koji omogućavaju fine-grained kontrolu nad svakim AI servisom. To će učiniti API još sigurnijim i lakšim za korišćenje!
📚 REČNIK DANAŠNJE LEKCIJE:
- Request Routing: Proces usmeravanja zahteva ka odgovarajućem servisu
- Structured Request: Zahtev sa jasno definisanim tipom i kontekstom
- Request Type: Kategorija zahteva (chat, code, debug, itd.)
- Routing Strategy: Algoritam za izbor providera
- Static Routing: Fiksna pravila rutiranja
- Dynamic Routing: Rutiranje na osnovu trenutnog stanja
- Load Balancing: Ravnomerna distribucija opterećenja
- Hybrid Strategy: Kombinacija više pristupa
- Request Context: Dodatne informacije uz zahtev
- Provider Override: Eksplicitno biranje providera
- Request Analyzer: Komponenta koja prepoznaje tip zahteva
- Routing Metadata: Informacije o tome kako je doneta odluka