diff --git a/apps/desktop/assets/codex-bridge/kopru.py b/apps/desktop/assets/codex-bridge/kopru.py index 2b28880..0a3c731 100644 --- a/apps/desktop/assets/codex-bridge/kopru.py +++ b/apps/desktop/assets/codex-bridge/kopru.py @@ -875,22 +875,28 @@ class H(http.server.BaseHTTPRequestHandler): """Katman denetimi (jev_denetim.py): cevap istemciye verilmeden once tamami okunur ve Jev'e sorulur. Gecerse oldugu gibi verilir; gecmezse atilir ve ayni adimi siradaki katman yapar (ac(ad): (resp, rewriter, tag)). Son katmanin cevabi sorulmadan akar. Beklerken SSE yorum satiri gider ki Codex akisi kopmus saymasin.""" - self._akis_basligi(resp, resp.status) - kalan, son_yazi = list(sira), time.time() + if resp is None: # secili model hata verdi: cevabi yok, adimi siradaki katman yapar + self.send_response(200) + self.send_header("Content-Type", "text/event-stream") + self.send_header("Transfer-Encoding", "chunked") + self.end_headers() + else: + self._akis_basligi(resp, resp.status) + kalan, son_yazi, son_hata = list(sira), time.time(), "" def w(b): if b: self.wfile.write(b"%x\r\n" % len(b) + b + b"\r\n"); self.wfile.flush() try: while True: - parcalar, okuyan = [], yarim.Izleyici("", "") - while True: + parcalar, okuyan, hata = [], yarim.Izleyici("", ""), resp is None + while not hata: try: chunk = resp.read1(4096) except (OSError, http.client.HTTPException) as e: - log(f"{tag} ust kaynak koptu (denetim tamponu, {sum(map(len, parcalar))} bayt): {type(e).__name__}: {e}") - self.close_connection = True - return + log(f"{tag} katman denetimi: cevap yarida koptu, siradakine geciliyor ({type(e).__name__}: {e})") + hata = True + break if not chunk: break parcalar.append(chunk) @@ -901,7 +907,9 @@ class H(http.server.BaseHTTPRequestHandler): # Her adim Jev'e gider: yazi yoksa modelin bu adimda cagirdigi araclar sorulur. metin = yazi or jev_denetim.komutlar(b"".join(parcalar)) gecmedi = False - if metin: + if hata: + gecmedi = True + elif metin: t0 = time.time() puan = jev_denetim.sor(istek, metin, jev_denetim.anahtar()) if puan is None: @@ -922,23 +930,32 @@ class H(http.server.BaseHTTPRequestHandler): try: yeni = ac(ad) except Exception as e: - log(f"{tag} katman denetimi: {ad} acilamadi ({type(e).__name__}: {str(e)[:120]})") + govde = e.read().decode("utf-8", "replace") if hasattr(e, "read") else "" + son_hata = f"{ad}: {e} {govde}".strip() + log(f"{tag} katman denetimi: {ad} acilamadi ({son_hata[:300]})") if yeni is None: break resp, rewriter, tag = yeni log(f"{tag} katman denetimi: adim bu katmana verildi") if not kalan: return self._stream(resp, resp.status, tag, rewriter, izle, basliksiz=True) - for chunk in parcalar: - if izle is not None: - izle.feed(chunk) - w(rewriter.feed(chunk) if rewriter else chunk) - if rewriter: w(rewriter.flush()) + if gecmedi: + # Siradakine gecilen adimda onceki model bir sey soyleyemez: son katmanin kendi hatasi gider. + log(f"{tag} katman denetimi: son katman da acilamadi, onun hatasi veriliyor") + olay = {"type": "response.failed", "response": {"id": "resp_astrabridge_katman", "object": "response", + "status": "failed", "output": [], "error": {"code": "katman_hatasi", "message": son_hata}}} + w(b"event: response.failed\ndata: " + json.dumps(olay, ensure_ascii=False).encode("utf-8") + b"\n\n") + else: + for chunk in parcalar: + if izle is not None: + izle.feed(chunk) + w(rewriter.feed(chunk) if rewriter else chunk) + if rewriter: w(rewriter.flush()) self.wfile.write(b"0\r\n\r\n"); self.wfile.flush() except (ConnectionAbortedError, BrokenPipeError, ConnectionResetError): log(f"{tag} istemci baglantiyi kapatti (denetim tamponu)") return - log(f"{tag} bitti status={resp.status} {sum(map(len, parcalar))} bayt") + log(f"{tag} bitti {'hata' if gecmedi else 'status=200'} {sum(map(len, parcalar))} bayt") def _stream(self, resp, status, tag, rewriter=None, izle=None, basliksiz=False): if not basliksiz: @@ -1620,6 +1637,8 @@ class H(http.server.BaseHTTPRequestHandler): log(f"{tag} deneme {attempt}: HTTP {e.code} {errbody[:100]!r} -> tekrar") time.sleep(min(2 * attempt, 8)); continue log(f"{tag} HTTP {e.code} {errbody[:200]!r}") + if jev_sira: # secili model hata verdi: adimi siradaki katman yapar + return self._denetimli_akis(None, tag, rewriter, izle, jev_istek, jev_sira, jev_ac) if pencere.tasma_mi(etxt): # Hedef "baglam sigmiyor" dedi: Codex'in tanidigi tasma hatasi olarak iletilir ve hedefin # bildirdigi gercek sinir ogrenilir (sonraki model listesinde pencere kuculur). @@ -1651,6 +1670,8 @@ class H(http.server.BaseHTTPRequestHandler): if jev_sira and resp.status == 200: return self._denetimli_akis(resp, tag, rewriter, izle, jev_istek, jev_sira, jev_ac) return self._stream(resp, resp.status, tag, rewriter, izle) + if jev_sira: # secili modele ulasilamadi: adimi siradaki katman yapar + return self._denetimli_akis(None, tag, rewriter, izle, jev_istek, jev_sira, jev_ac) self._send_json(503, {"error": {"message": f"bridge: hedef ulasilamadi {last}"}}) do_POST = _handle diff --git a/apps/desktop/assets/codex-bridge/test_jev_denetim.py b/apps/desktop/assets/codex-bridge/test_jev_denetim.py index 60c2208..bb7f50c 100644 --- a/apps/desktop/assets/codex-bridge/test_jev_denetim.py +++ b/apps/desktop/assets/codex-bridge/test_jev_denetim.py @@ -241,6 +241,31 @@ class Akis(unittest.TestCase): self.assertEqual(self.opened, ["Katman-2", "Katman-3"]) self.assertEqual(self.sent(), self.cevap["Katman-3"]) + def test_an_error_of_the_selected_model_goes_to_the_next_layer(self): + self.h._denetimli_akis(None, "[1]", None, None, "yap", ["Katman-1", "Katman-2", "Katman-3", "Katman-4"], self.ac) + self.assertEqual(self.opened, ["Katman-1"]) + self.assertEqual(self.sent(), self.cevap["Katman-1"]) + + def test_an_answer_cut_midway_goes_to_the_next_layer(self): + class Kopan(Yanit): + def read1(self, n): + if not self.parts: + raise OSError("koptu") + return self.parts.pop(0) + self.h._denetimli_akis(Kopan(sse("yarim")), "[1]", None, None, "yap", ["Katman-2", "Katman-3", "Katman-4"], self.ac) + self.assertEqual(self.opened, ["Katman-2"]) + self.assertEqual(self.sent(), self.cevap["Katman-2"]) + self.assertNotIn(b"yarim", self.h.wfile.getvalue()) + + def test_when_no_layer_opens_the_blocked_answer_is_not_delivered(self): + self.puanlar = [{"ret": 0.9, "sart": 0.0}] + self.cevap.update({"Katman-2": None, "Katman-3": None, "Katman-4": None}) + self.calistir(sse("Bunu yapamam.")) + self.assertEqual(self.opened, ["Katman-2", "Katman-3", "Katman-4"]) + self.assertNotIn(b"yapamam", self.h.wfile.getvalue()) + self.assertIn(b"response.failed", self.sent()) + self.assertIn(b"Katman-4: acilamadi", self.sent()) # the last layer's own error reaches the user + def test_unreachable_jev_keeps_the_answer(self): self.puanlar = [None] body = sse("Bunu yapamam.")