AI Asistent Učitelj Vasa: Praktična obuka za početnike (32 dana)

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:

  1. Type Safety – znamo tačno šta očekujemo
  2. Routing Logic – možemo da biramo provider po tipu
  3. Optimization – različiti parametri za različite zadatke
  4. Analytics – lakše praćenje šta korisnici najviše traže
  5. 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:

  1. Zašto RequestAnalyzer povećava skor za CODE_DEBUG kad nađe ““`” u tekstu?
  2. 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:

  1. Static – fiksna pravila (code → OpenAI)
  2. Dynamic – na osnovu trenutnog stanja
  3. Learning – uči iz rezultata
  4. 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:

  1. Koja dva uslova moraju biti ispunjena da bi LoadBalancingStrategy izabrala random.choice između kandidata?
  2. Š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:

  1. /pitaj endpoint prepoznaje tip pitanja
  2. Automatski bira najbolji AI servis
  3. Prikazuje korisniku koji servis je korišćen i zašto
  4. 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: RoutingStrategy je interfejs, a StaticRoutingStrategy, PerformanceRoutingStrategy itd. su konkretne implementacije. Ovo nam omogućava da menjamo algoritam rutiranja u letu bez izmene glavne logike routera.
  • Facade Pattern: SmartProviderRouter služi kao jedinstvena, pojednostavljena tačka interakcije (route_request metoda) 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:

  1. Preferira OpenAI ujutru (pre 12h) za “svežinu”
  2. Preferira Gemini popodne za “brzinu”
  3. Vikendom uvek koristi jeftiniji servis

🔄 POKUŠAJ SAM:

  1. Pronađi u test-fajlu test_routing.py scenario 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_metadata u JSON fajl latest_route.json.
  2. 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]:

  1. Koji su osnovni tipovi zahteva koje Vasa prepoznaje?
  2. Šta je provider routing i zašto je koristan?
  3. Kako strukturiran zahtev pomaže u boljem odgovoru?
  4. Šta znači “override” provider opcija?

[NIVO 2]:

  1. Koje su prednosti TYPE_KEYWORDS pristupa i koja su ograničenja?
  2. Kako bi implementirao A/B testing kroz routing?
  3. Šta je hybrid routing strategija i kada je korisna?
  4. Kako bi dodao podršku za novi tip zahteva?
  5. Koje metrike bi pratio za optimizaciju routinga?

🤔 MINI-KVIZ:

  1. Koja routing strategija bi bila najbolja za produkciju sa 10000+ zahteva dnevno?
  2. Kako bi implementirao “sticky sessions” – da korisnik uvek ide na isti provider tokom sesije?
  3. Š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