Spaces:
Paused
Paused
Update deepseek_client.py
Browse files- deepseek_client.py +211 -1
deepseek_client.py
CHANGED
|
@@ -100,4 +100,214 @@ class DeepSeekClient:
|
|
| 100 |
response_data = response.json()
|
| 101 |
|
| 102 |
# Sprawd藕 struktur臋 odpowiedzi
|
| 103 |
-
if 'choices' not in
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 100 |
response_data = response.json()
|
| 101 |
|
| 102 |
# Sprawd藕 struktur臋 odpowiedzi
|
| 103 |
+
if 'choices' not in response_data or not response_data['choices']:
|
| 104 |
+
raise ValueError("Nieprawid艂owa struktura odpowiedzi: brak choices")
|
| 105 |
+
|
| 106 |
+
choice = response_data['choices'][0]
|
| 107 |
+
if 'message' not in choice or 'content' not in choice['message']:
|
| 108 |
+
raise ValueError("Nieprawid艂owa struktura odpowiedzi: brak message/content")
|
| 109 |
+
|
| 110 |
+
content = choice['message']['content']
|
| 111 |
+
|
| 112 |
+
# Pr贸ba parsowania JSON
|
| 113 |
+
try:
|
| 114 |
+
parsed_content = json.loads(content)
|
| 115 |
+
return parsed_content
|
| 116 |
+
except json.JSONDecodeError as e:
|
| 117 |
+
self.logger.error(f"Nie uda艂o si臋 sparsowa膰 odpowiedzi jako JSON: {content}")
|
| 118 |
+
raise ValueError(f"Odpowied藕 nie jest prawid艂owym JSON: {str(e)}")
|
| 119 |
+
|
| 120 |
+
except (httpx.HTTPError, ValueError) as e:
|
| 121 |
+
last_error = e
|
| 122 |
+
retries += 1
|
| 123 |
+
|
| 124 |
+
if retries < max_retries:
|
| 125 |
+
# Exponential backoff z jitterem
|
| 126 |
+
wait_time = retry_delay * (2 ** (retries - 1)) * (0.5 + random.random())
|
| 127 |
+
self.logger.warning(
|
| 128 |
+
f"Pr贸ba {retries} nie powiod艂a si臋: {str(e)}. "
|
| 129 |
+
f"Czekam {wait_time:.2f}s przed ponowieniem..."
|
| 130 |
+
)
|
| 131 |
+
await asyncio.sleep(wait_time)
|
| 132 |
+
continue
|
| 133 |
+
|
| 134 |
+
self.logger.error(f"Wszystkie {max_retries} pr贸b nie powiod艂y si臋")
|
| 135 |
+
break
|
| 136 |
+
|
| 137 |
+
raise last_error
|
| 138 |
+
|
| 139 |
+
async def analyze_criteria(self, brief_content: str) -> List[Dict]:
|
| 140 |
+
"""
|
| 141 |
+
Analizuje brief/SIWZ w poszukiwaniu kryteri贸w oceny.
|
| 142 |
+
|
| 143 |
+
Args:
|
| 144 |
+
brief_content: Tre艣膰 dokumentu do analizy
|
| 145 |
+
|
| 146 |
+
Returns:
|
| 147 |
+
List[Dict]: Lista wyodr臋bnionych kryteri贸w
|
| 148 |
+
|
| 149 |
+
Raises:
|
| 150 |
+
ValueError: Gdy nie uda艂o si臋 wyodr臋bni膰 kryteri贸w
|
| 151 |
+
"""
|
| 152 |
+
prompt = """
|
| 153 |
+
Przeanalizuj poni偶szy dokument przetargowy i zidentyfikuj kryteria oceny ofert.
|
| 154 |
+
Zwr贸膰 odpowied藕 dok艂adnie w poni偶szym formacie JSON (wa偶ne: odpowied藕 musi by膰 poprawnym JSON):
|
| 155 |
+
|
| 156 |
+
{
|
| 157 |
+
"criteria": [
|
| 158 |
+
{
|
| 159 |
+
"name": "nazwa kryterium",
|
| 160 |
+
"weight": liczba,
|
| 161 |
+
"description": "opis",
|
| 162 |
+
"scoring_guide": "wskaz贸wki do oceny"
|
| 163 |
+
}
|
| 164 |
+
]
|
| 165 |
+
}
|
| 166 |
+
|
| 167 |
+
Upewnij si臋, 偶e:
|
| 168 |
+
- Suma wag wszystkich kryteri贸w wynosi dok艂adnie 100%
|
| 169 |
+
- Ka偶de kryterium ma unikaln膮 nazw臋
|
| 170 |
+
- Opisy s膮 konkretne i jednoznaczne
|
| 171 |
+
- Format JSON jest 艣ci艣le zachowany
|
| 172 |
+
"""
|
| 173 |
+
|
| 174 |
+
try:
|
| 175 |
+
response = await self.analyze_text(prompt, brief_content)
|
| 176 |
+
|
| 177 |
+
if not isinstance(response, dict) or 'criteria' not in response:
|
| 178 |
+
raise ValueError("Nieprawid艂owa struktura odpowiedzi od modelu")
|
| 179 |
+
|
| 180 |
+
criteria = response['criteria']
|
| 181 |
+
await self._validate_criteria(criteria)
|
| 182 |
+
return criteria
|
| 183 |
+
|
| 184 |
+
except Exception as e:
|
| 185 |
+
self.logger.error(f"B艂膮d podczas parsowania kryteri贸w: {str(e)}")
|
| 186 |
+
raise
|
| 187 |
+
|
| 188 |
+
async def analyze_offer(
|
| 189 |
+
self,
|
| 190 |
+
offer_content: str,
|
| 191 |
+
criteria: List[Dict],
|
| 192 |
+
brief_content: str
|
| 193 |
+
) -> Dict:
|
| 194 |
+
"""
|
| 195 |
+
Analizuje ofert臋 wzgl臋dem zadanych kryteri贸w.
|
| 196 |
+
|
| 197 |
+
Args:
|
| 198 |
+
offer_content: Tre艣膰 oferty do analizy
|
| 199 |
+
criteria: Lista kryteri贸w oceny
|
| 200 |
+
brief_content: Tre艣膰 briefu/SIWZ
|
| 201 |
+
|
| 202 |
+
Returns:
|
| 203 |
+
Dict: Analiza oferty z ocenami i uzasadnieniami
|
| 204 |
+
|
| 205 |
+
Raises:
|
| 206 |
+
ValueError: Gdy nie uda艂o si臋 przeanalizowa膰 oferty
|
| 207 |
+
"""
|
| 208 |
+
prompt = f"""
|
| 209 |
+
Oce艅 poni偶sz膮 ofert臋 wzgl臋dem kryteri贸w. Zwr贸膰 odpowied藕 dok艂adnie w poni偶szym formacie JSON:
|
| 210 |
+
|
| 211 |
+
{{
|
| 212 |
+
"evaluations": [
|
| 213 |
+
{{
|
| 214 |
+
"criterion_name": "nazwa kryterium",
|
| 215 |
+
"score": liczba 0-100,
|
| 216 |
+
"justification": "szczeg贸艂owe uzasadnienie",
|
| 217 |
+
"key_points": ["g艂贸wny punkt 1", "g艂贸wny punkt 2"],
|
| 218 |
+
"evidence": ["cytat/referencja 1", "cytat/referencja 2"]
|
| 219 |
+
}}
|
| 220 |
+
],
|
| 221 |
+
"strengths": ["mocna strona 1", "mocna strona 2"],
|
| 222 |
+
"weaknesses": ["s艂aba strona 1", "s艂aba strona 2"],
|
| 223 |
+
"summary": "kr贸tkie podsumowanie oceny",
|
| 224 |
+
"recommendations": ["rekomendacja 1", "rekomendacja 2"]
|
| 225 |
+
}}
|
| 226 |
+
|
| 227 |
+
Kryteria oceny:
|
| 228 |
+
{json.dumps(criteria, indent=2, ensure_ascii=False)}
|
| 229 |
+
|
| 230 |
+
Kontekst z briefu/SIWZ:
|
| 231 |
+
{brief_content[:1000]}...
|
| 232 |
+
"""
|
| 233 |
+
|
| 234 |
+
response = await self.analyze_text(prompt, offer_content)
|
| 235 |
+
try:
|
| 236 |
+
await self._validate_analysis(response)
|
| 237 |
+
return response
|
| 238 |
+
except Exception as e:
|
| 239 |
+
self.logger.error(f"B艂膮d podczas parsowania analizy oferty: {str(e)}")
|
| 240 |
+
raise
|
| 241 |
+
|
| 242 |
+
async def _validate_criteria(self, criteria: List[Dict]) -> None:
|
| 243 |
+
"""
|
| 244 |
+
Waliduje wyodr臋bnione kryteria.
|
| 245 |
+
|
| 246 |
+
Args:
|
| 247 |
+
criteria: Lista kryteri贸w do walidacji
|
| 248 |
+
|
| 249 |
+
Raises:
|
| 250 |
+
ValueError: Gdy kryteria nie spe艂niaj膮 wymaga艅
|
| 251 |
+
"""
|
| 252 |
+
if not criteria:
|
| 253 |
+
raise ValueError("Nie znaleziono 偶adnych kryteri贸w")
|
| 254 |
+
|
| 255 |
+
# Sprawd藕 sum臋 wag
|
| 256 |
+
total_weight = sum(c['weight'] for c in criteria)
|
| 257 |
+
if abs(total_weight - 100) > 0.01:
|
| 258 |
+
self.logger.warning(
|
| 259 |
+
f"Suma wag kryteri贸w ({total_weight}%) r贸偶ni si臋 od 100%"
|
| 260 |
+
)
|
| 261 |
+
|
| 262 |
+
# Sprawd藕 unikalno艣膰 nazw
|
| 263 |
+
names = [c['name'] for c in criteria]
|
| 264 |
+
if len(names) != len(set(names)):
|
| 265 |
+
raise ValueError("Znaleziono duplikaty w nazwach kryteri贸w")
|
| 266 |
+
|
| 267 |
+
# Sprawd藕 wymagane pola
|
| 268 |
+
required_fields = {'name', 'weight', 'description'}
|
| 269 |
+
for criterion in criteria:
|
| 270 |
+
missing_fields = required_fields - set(criterion.keys())
|
| 271 |
+
if missing_fields:
|
| 272 |
+
raise ValueError(f"Brakuj膮ce pola w kryterium: {missing_fields}")
|
| 273 |
+
|
| 274 |
+
async def _validate_analysis(self, analysis: Dict) -> None:
|
| 275 |
+
"""
|
| 276 |
+
Waliduje analiz臋 oferty.
|
| 277 |
+
|
| 278 |
+
Args:
|
| 279 |
+
analysis: Analiza do walidacji
|
| 280 |
+
|
| 281 |
+
Raises:
|
| 282 |
+
ValueError: Gdy analiza nie spe艂nia wymaga艅
|
| 283 |
+
"""
|
| 284 |
+
required_fields = {
|
| 285 |
+
'evaluations', 'strengths', 'weaknesses',
|
| 286 |
+
'summary', 'recommendations'
|
| 287 |
+
}
|
| 288 |
+
|
| 289 |
+
missing_fields = required_fields - set(analysis.keys())
|
| 290 |
+
if missing_fields:
|
| 291 |
+
raise ValueError(f"Brakuj膮ce pola w analizie: {missing_fields}")
|
| 292 |
+
|
| 293 |
+
if not analysis['evaluations']:
|
| 294 |
+
raise ValueError("Brak ocen w analizie")
|
| 295 |
+
|
| 296 |
+
for eval in analysis['evaluations']:
|
| 297 |
+
if not (0 <= eval['score'] <= 100):
|
| 298 |
+
raise ValueError(
|
| 299 |
+
f"Nieprawid艂owa ocena: {eval['score']} "
|
| 300 |
+
f"dla kryterium {eval['criterion_name']}"
|
| 301 |
+
)
|
| 302 |
+
|
| 303 |
+
async def close(self):
|
| 304 |
+
"""Zamyka klienta HTTP"""
|
| 305 |
+
await self.client.aclose()
|
| 306 |
+
|
| 307 |
+
async def __aenter__(self):
|
| 308 |
+
"""Context manager entry"""
|
| 309 |
+
return self
|
| 310 |
+
|
| 311 |
+
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
| 312 |
+
"""Context manager exit"""
|
| 313 |
+
await self.close()
|