WhizTenderBot1.0 / deepseek_client.py
Marek4321's picture
Update deepseek_client.py
172441b verified
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()