Spaces:
Paused
Paused
| import random | |
| import os | |
| from typing import Dict, List, Optional, Any | |
| import logging | |
| import json | |
| import asyncio | |
| from pathlib import Path | |
| import httpx | |
| class DeepSeekClient: | |
| """ | |
| Asynchroniczny klient do komunikacji z API DeepSeek dla analizy dokument贸w przetargowych | |
| """ | |
| def __init__(self, api_key: str, base_url: str): | |
| """ | |
| Inicjalizuje klienta DeepSeek. | |
| Args: | |
| api_key: Klucz API do DeepSeek | |
| base_url: Bazowy URL API DeepSeek | |
| """ | |
| self.api_key = api_key | |
| self.base_url = base_url.rstrip('/') | |
| self.logger = logging.getLogger(__name__) | |
| # Konfiguracja asynchronicznego klienta HTTP | |
| self.client = httpx.AsyncClient( | |
| base_url=self.base_url, | |
| headers={ | |
| 'Authorization': f'Bearer {self.api_key}', | |
| 'Content-Type': 'application/json' | |
| }, | |
| timeout=60.0 | |
| ) | |
| # Domy艣lne parametry dla zapyta艅 | |
| self.default_params = { | |
| 'temperature': 0.3, | |
| 'max_tokens': 2000, | |
| 'model': 'deepseek-chat' | |
| } | |
| async def analyze_text( | |
| self, | |
| prompt: str, | |
| content: str, | |
| temperature: float = 0.3, | |
| max_tokens: int = 2000, | |
| max_retries: int = 3, | |
| retry_delay: float = 1.0 | |
| ) -> Dict: | |
| """ | |
| Analizuje tekst u偶ywaj膮c API DeepSeek. | |
| Args: | |
| prompt: Prompt dla modelu | |
| content: Tekst do analizy | |
| temperature: Temperatura generowania (0-1) | |
| max_tokens: Maksymalna d艂ugo艣膰 odpowiedzi | |
| max_retries: Maksymalna liczba pr贸b | |
| retry_delay: Podstawowe op贸藕nienie mi臋dzy pr贸bami (w sekundach) | |
| Returns: | |
| Dict: Odpowied藕 od API | |
| Raises: | |
| ValueError: Gdy odpowied藕 ma nieprawid艂ow膮 struktur臋 | |
| httpx.HTTPError: Przy b艂臋dach komunikacji z API | |
| """ | |
| retries = 0 | |
| last_error = None | |
| while retries < max_retries: | |
| try: | |
| request_data = { | |
| 'model': self.default_params['model'], | |
| 'messages': [ | |
| { | |
| 'role': 'system', | |
| 'content': 'Jeste艣 ekspertem w analizie dokument贸w przetargowych. ' | |
| 'Zawsze odpowiadasz w poprawnym formacie JSON.' | |
| }, | |
| { | |
| 'role': 'user', | |
| 'content': f"{prompt}\n\nTekst do analizy:\n{content}" | |
| } | |
| ], | |
| 'temperature': temperature, | |
| 'max_tokens': max_tokens, | |
| 'response_format': {"type": "json_object"} | |
| } | |
| self.logger.debug(f"Wysy艂anie zapytania do API (pr贸ba {retries + 1}/{max_retries})") | |
| response = await self.client.post( # U偶yj self.client bez otwierania nowego kontekstu | |
| '/v1/chat/completions', | |
| json=request_data | |
| ) | |
| response.raise_for_status() | |
| response_data = response.json() | |
| # Sprawd藕 struktur臋 odpowiedzi | |
| if 'choices' not in response_data or not response_data['choices']: | |
| raise ValueError("Nieprawid艂owa struktura odpowiedzi: brak choices") | |
| choice = response_data['choices'][0] | |
| if 'message' not in choice or 'content' not in choice['message']: | |
| raise ValueError("Nieprawid艂owa struktura odpowiedzi: brak message/content") | |
| content = choice['message']['content'] | |
| # Pr贸ba parsowania JSON | |
| try: | |
| parsed_content = json.loads(content) | |
| return parsed_content | |
| except json.JSONDecodeError as e: | |
| self.logger.error(f"Nie uda艂o si臋 sparsowa膰 odpowiedzi jako JSON: {content}") | |
| raise ValueError(f"Odpowied藕 nie jest prawid艂owym JSON: {str(e)}") | |
| except (httpx.HTTPError, ValueError) as e: | |
| last_error = e | |
| retries += 1 | |
| if retries < max_retries: | |
| # Exponential backoff z jitterem | |
| wait_time = retry_delay * (2 ** (retries - 1)) * (0.5 + random.random()) | |
| self.logger.warning( | |
| f"Pr贸ba {retries} nie powiod艂a si臋: {str(e)}. " | |
| f"Czekam {wait_time:.2f}s przed ponowieniem..." | |
| ) | |
| await asyncio.sleep(wait_time) | |
| continue | |
| self.logger.error(f"Wszystkie {max_retries} pr贸b nie powiod艂y si臋") | |
| break | |
| raise last_error | |
| async def analyze_criteria(self, brief_content: str) -> List[Dict]: | |
| """ | |
| Analizuje brief/SIWZ w poszukiwaniu kryteri贸w oceny. | |
| Args: | |
| brief_content: Tre艣膰 dokumentu do analizy | |
| Returns: | |
| List[Dict]: Lista wyodr臋bnionych kryteri贸w | |
| Raises: | |
| ValueError: Gdy nie uda艂o si臋 wyodr臋bni膰 kryteri贸w | |
| """ | |
| prompt = """ | |
| Przeanalizuj poni偶szy dokument przetargowy i zidentyfikuj kryteria oceny ofert. | |
| Zwr贸膰 odpowied藕 dok艂adnie w poni偶szym formacie JSON (wa偶ne: odpowied藕 musi by膰 poprawnym JSON): | |
| { | |
| "criteria": [ | |
| { | |
| "name": "nazwa kryterium", | |
| "weight": liczba, | |
| "description": "opis", | |
| "scoring_guide": "wskaz贸wki do oceny" | |
| } | |
| ] | |
| } | |
| Upewnij si臋, 偶e: | |
| - Suma wag wszystkich kryteri贸w wynosi dok艂adnie 100% | |
| - Ka偶de kryterium ma unikaln膮 nazw臋 | |
| - Opisy s膮 konkretne i jednoznaczne | |
| - Format JSON jest 艣ci艣le zachowany | |
| """ | |
| try: | |
| response = await self.analyze_text(prompt, brief_content) | |
| if not isinstance(response, dict) or 'criteria' not in response: | |
| raise ValueError("Nieprawid艂owa struktura odpowiedzi od modelu") | |
| criteria = response['criteria'] | |
| await self._validate_criteria(criteria) | |
| return criteria | |
| except Exception as e: | |
| self.logger.error(f"B艂膮d podczas parsowania kryteri贸w: {str(e)}") | |
| raise | |
| async def analyze_offer( | |
| self, | |
| offer_content: str, | |
| criteria: List[Dict], | |
| brief_content: str | |
| ) -> Dict: | |
| """ | |
| Analizuje ofert臋 wzgl臋dem zadanych kryteri贸w. | |
| Args: | |
| offer_content: Tre艣膰 oferty do analizy | |
| criteria: Lista kryteri贸w oceny | |
| brief_content: Tre艣膰 briefu/SIWZ | |
| Returns: | |
| Dict: Analiza oferty z ocenami i uzasadnieniami | |
| Raises: | |
| ValueError: Gdy nie uda艂o si臋 przeanalizowa膰 oferty | |
| """ | |
| prompt = f""" | |
| Oce艅 poni偶sz膮 ofert臋 wzgl臋dem kryteri贸w. Zwr贸膰 odpowied藕 dok艂adnie w poni偶szym formacie JSON: | |
| {{ | |
| "evaluations": [ | |
| {{ | |
| "criterion_name": "nazwa kryterium", | |
| "score": liczba 0-100, | |
| "justification": "szczeg贸艂owe uzasadnienie", | |
| "key_points": ["g艂贸wny punkt 1", "g艂贸wny punkt 2"], | |
| "evidence": ["cytat/referencja 1", "cytat/referencja 2"] | |
| }} | |
| ], | |
| "strengths": ["mocna strona 1", "mocna strona 2"], | |
| "weaknesses": ["s艂aba strona 1", "s艂aba strona 2"], | |
| "summary": "kr贸tkie podsumowanie oceny", | |
| "recommendations": ["rekomendacja 1", "rekomendacja 2"] | |
| }} | |
| Kryteria oceny: | |
| {json.dumps(criteria, indent=2, ensure_ascii=False)} | |
| Kontekst z briefu/SIWZ: | |
| {brief_content[:1000]}... | |
| """ | |
| response = await self.analyze_text(prompt, offer_content) | |
| try: | |
| await self._validate_analysis(response) | |
| return response | |
| except Exception as e: | |
| self.logger.error(f"B艂膮d podczas parsowania analizy oferty: {str(e)}") | |
| raise | |
| async def _validate_criteria(self, criteria: List[Dict]) -> None: | |
| """ | |
| Waliduje wyodr臋bnione kryteria. | |
| Args: | |
| criteria: Lista kryteri贸w do walidacji | |
| Raises: | |
| ValueError: Gdy kryteria nie spe艂niaj膮 wymaga艅 | |
| """ | |
| if not criteria: | |
| raise ValueError("Nie znaleziono 偶adnych kryteri贸w") | |
| # Sprawd藕 sum臋 wag | |
| total_weight = sum(c['weight'] for c in criteria) | |
| if abs(total_weight - 100) > 0.01: | |
| self.logger.warning( | |
| f"Suma wag kryteri贸w ({total_weight}%) r贸偶ni si臋 od 100%" | |
| ) | |
| # Sprawd藕 unikalno艣膰 nazw | |
| names = [c['name'] for c in criteria] | |
| if len(names) != len(set(names)): | |
| raise ValueError("Znaleziono duplikaty w nazwach kryteri贸w") | |
| # Sprawd藕 wymagane pola | |
| required_fields = {'name', 'weight', 'description'} | |
| for criterion in criteria: | |
| missing_fields = required_fields - set(criterion.keys()) | |
| if missing_fields: | |
| raise ValueError(f"Brakuj膮ce pola w kryterium: {missing_fields}") | |
| async def _validate_analysis(self, analysis: Dict) -> None: | |
| """ | |
| Waliduje analiz臋 oferty. | |
| Args: | |
| analysis: Analiza do walidacji | |
| Raises: | |
| ValueError: Gdy analiza nie spe艂nia wymaga艅 | |
| """ | |
| required_fields = { | |
| 'evaluations', 'strengths', 'weaknesses', | |
| 'summary', 'recommendations' | |
| } | |
| missing_fields = required_fields - set(analysis.keys()) | |
| if missing_fields: | |
| raise ValueError(f"Brakuj膮ce pola w analizie: {missing_fields}") | |
| if not analysis['evaluations']: | |
| raise ValueError("Brak ocen w analizie") | |
| for eval in analysis['evaluations']: | |
| if not (0 <= eval['score'] <= 100): | |
| raise ValueError( | |
| f"Nieprawid艂owa ocena: {eval['score']} " | |
| f"dla kryterium {eval['criterion_name']}" | |
| ) | |
| async def close(self): | |
| """Zamyka klienta HTTP""" | |
| await self.client.aclose() | |
| async def __aenter__(self): | |
| """Context manager entry""" | |
| return self | |
| async def __aexit__(self, exc_type, exc_val, exc_tb): | |
| """Context manager exit""" | |
| await self.close() |