From 3726d2a5538ddf566d64f6d7ced3f42f6625f0c0 Mon Sep 17 00:00:00 2001 From: yilsem Date: Sun, 11 Oct 2026 15:51:43 +0300 Subject: [PATCH] fix(codex-bridge): preserve tool model and stream preparation without Jev fallback --- apps/desktop/assets/codex-bridge/ara.py | 17 +++++- apps/desktop/assets/codex-bridge/internet.py | 6 +-- .../assets/codex-bridge/jev_denetim.py | 26 ++++++++++ apps/desktop/assets/codex-bridge/kopru.py | 13 ++++- .../assets/codex-bridge/test_internet.py | 18 ++++++- .../assets/codex-bridge/test_jev_denetim.py | 52 ++++++++++++++++--- 6 files changed, 118 insertions(+), 14 deletions(-) diff --git a/apps/desktop/assets/codex-bridge/ara.py b/apps/desktop/assets/codex-bridge/ara.py index 60a99bd..f2be62b 100644 --- a/apps/desktop/assets/codex-bridge/ara.py +++ b/apps/desktop/assets/codex-bridge/ara.py @@ -13,6 +13,7 @@ import re import sys import urllib.parse import urllib.request +import xml.etree.ElementTree as ET UA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0 Safari/537.36" ARAMA = "https://html.duckduckgo.com/html/?" @@ -73,7 +74,21 @@ def ara(sorgu, adet=8, indirici=indir): sayfa, _ = indirici(ARAMA + urllib.parse.urlencode({"q": sorgu})) sonuc = sonuclari_coz(sayfa, adet) if not sonuc: - return "Sonuç bulunamadı. Sorguyu daha kısa ya da farklı sözcüklerle yeniden dene." + if 'no-results' in sayfa.lower() or 'no results found' in sayfa.lower(): + return "Sonuç bulunamadı. En fazla bir farklı sorgu deneyebilirsin." + # DDG bot kontrolünü/bozuk HTML'yi sonuç yok diye göstermeyiz. Tek yedek istek. + try: + rss, _ = indirici('https://www.bing.com/search?' + urllib.parse.urlencode({'format': 'rss', 'q': sorgu})) + kok = ET.fromstring(rss) + if kok.tag != 'rss' or kok.find('channel') is None: + raise ValueError('RSS arama yanıtı değil') + sonuc = [(i.findtext('title') or '', i.findtext('link') or '', _duz(i.findtext('description') or '')) + for i in kok.findall('./channel/item') + if (i.findtext('link') or '').startswith(('https://', 'http://'))][:adet] + except (ValueError, ET.ParseError, OSError) as hata: + raise RuntimeError('Arama hizmeti engellendi veya yanıtı okunamadı; sorguları değiştirerek tekrar etme.') from hata + if not sonuc: + return "Sonuç bulunamadı. En fazla bir farklı sorgu deneyebilirsin." return "\n\n".join(f"{i}. {b}\n {a}\n {o}" for i, (b, a, o) in enumerate(sonuc, 1)) diff --git a/apps/desktop/assets/codex-bridge/internet.py b/apps/desktop/assets/codex-bridge/internet.py index 70c9c77..697b693 100644 --- a/apps/desktop/assets/codex-bridge/internet.py +++ b/apps/desktop/assets/codex-bridge/internet.py @@ -18,8 +18,8 @@ VARSAYILAN = ( "Arama: {ara} \"sorgu\"\n" "Sayfa okuma: {oku} \n" "Önce ara, sonra işe yarayan iki üç sonucu oku, cevabında kaynak adreslerini belirt. Başka bir arama aracı " - "arama (tool_search ile bulunmaz). Komut hata verirse sorguyu değiştirip bir kez daha dene; yine olmazsa " - "aramanın yapılamadığını açıkça söyle, bilgi uydurma." + "arama (tool_search ile bulunmaz). Arama hizmeti engeli veya ayrıştırma hatasında sorguları tekrar tekrar " + "değiştirme; aramanın yapılamadığını açıkça söyle. Gerçek sonuç yoksa en fazla bir farklı sorgu dene. Bilgi uydurma." ) @@ -39,7 +39,7 @@ def talimat(ayar, betik, python=None): ayar = ayar if isinstance(ayar, dict) else {} if ayar.get("enabled") is False or not betik or not os.path.isfile(betik): return None - on = f'"{python or python_yolu()}" "{betik}"' + on = f'& "{python or python_yolu()}" "{betik}"' if os.name == 'nt' else f'"{python or python_yolu()}" "{betik}"' return str(ayar.get("talimat") or VARSAYILAN).replace("{ara}", on + " ara").replace("{oku}", on + " oku") diff --git a/apps/desktop/assets/codex-bridge/jev_denetim.py b/apps/desktop/assets/codex-bridge/jev_denetim.py index 48bf9ac..47b667c 100644 --- a/apps/desktop/assets/codex-bridge/jev_denetim.py +++ b/apps/desktop/assets/codex-bridge/jev_denetim.py @@ -93,6 +93,32 @@ def son_istek(req): return "" +def hazirlik_mi(govde): + """Araç çağrısı veya commentary görüldüyse adım nihai cevap değildir. + + Tam SSE satırları üzerinden bakılır; parçalı olay bir sonraki parçada tamamlanır. + Araç argümanında geçen olay adı hazırlık sayılmaz. + """ + for satir in govde.splitlines(): + if not satir.startswith(b'data:'): + continue + try: + olay = json.loads(satir[5:]) + except (ValueError, UnicodeDecodeError): + continue + if not isinstance(olay, dict): + continue + tip = str(olay.get('type') or '') + oge = olay.get('item') or {} + if tip in ('response.output_item.added', 'response.output_item.done') and isinstance(oge, dict): + if str(oge.get('type') or '').endswith('_call') or oge.get('phase') == 'commentary': + return True + if tip in ('response.function_call_arguments.delta', 'response.function_call_arguments.done', + 'response.custom_tool_call_input.delta', 'response.custom_tool_call_input.done'): + return True + return False + + def komutlar(govde): """Yazisi olmayan adimda Jev'e giden metin: modelin bu adimda cagirdigi araclar (ad ve arguman). govde: ust kaynagin SSE yaniti (bayt). Arac cagrisi yoksa "".""" diff --git a/apps/desktop/assets/codex-bridge/kopru.py b/apps/desktop/assets/codex-bridge/kopru.py index 1249d49..f1b7278 100644 --- a/apps/desktop/assets/codex-bridge/kopru.py +++ b/apps/desktop/assets/codex-bridge/kopru.py @@ -901,11 +901,20 @@ class H(http.server.BaseHTTPRequestHandler): break parcalar.append(chunk) okuyan.feed(chunk) + if jev_denetim.hazirlik_mi(b''.join(parcalar)): + # Kullanıcı kararı 11 Ekim: araç/hazırlık adımı ret denetimine girmez. + # İlk araç olayı görüldüğünde tamponu bırak; araç çağrısının bitmesini bekleme. + log(f"{tag} Jev atlandi: arac veya hazirlik adimi, secili model korunuyor") + for parca in parcalar: + if izle is not None: + izle.feed(parca) + w(rewriter.feed(parca) if rewriter else parca) + return self._stream(resp, resp.status, tag, rewriter, izle, basliksiz=True) if time.time() - son_yazi >= 10: w(b": denetim\n\n"); son_yazi = time.time() yazi = okuyan.metin.strip() - # Her adim Jev'e gider: yazi yoksa modelin bu adimda cagirdigi araclar sorulur. - metin = yazi or jev_denetim.komutlar(b"".join(parcalar)) + # Araç/hazırlık yukarıda aktarıldı; burada yalnız nihai cevap denetlenir. + metin = yazi gecmedi = False if hata: gecmedi = True diff --git a/apps/desktop/assets/codex-bridge/test_internet.py b/apps/desktop/assets/codex-bridge/test_internet.py index e02da11..3168311 100644 --- a/apps/desktop/assets/codex-bridge/test_internet.py +++ b/apps/desktop/assets/codex-bridge/test_internet.py @@ -28,7 +28,20 @@ class AraTest(unittest.TestCase): def test_search_output_and_empty_result(self): metin = ara.ara("x", indirici=lambda adres: (SAYFA, "text/html")) self.assertIn("1. Bir başlık\n https://ornek.com/bir?a=1", metin) - self.assertIn("Sonuç bulunamadı", ara.ara("x", indirici=lambda adres: ("", "text/html"))) + self.assertIn("Sonuç bulunamadı", ara.ara("x", indirici=lambda adres: ('
no results
', "text/html"))) + + def test_bot_challenge_uses_one_rss_backup_not_false_no_results(self): + calls = [] + def indirici(adres): + calls.append(adres) + return (('
Bot challenge
' if len(calls) == 1 else + 'OmniRoutehttps://ornek.com/'), 'text/html') + self.assertIn('https://ornek.com/', ara.ara('OmniRoute', indirici=indirici)) + self.assertEqual(len(calls), 2) + + def test_two_invalid_services_report_failure_not_no_results(self): + with self.assertRaisesRegex(RuntimeError, 'Arama hizmeti'): + ara.ara('x', indirici=lambda adres: ('challenge', 'text/html')) def test_page_is_turned_into_text_and_cut(self): sayfa = "

Bir & iki

üç
" @@ -46,7 +59,8 @@ class InternetTest(unittest.TestCase): betik = os.path.join(d, "ara.py") open(betik, "w").close() metin = internet.talimat(None, betik, python="py.exe") - self.assertIn(f'"py.exe" "{betik}" ara "sorgu"', metin) + on = '& ' if os.name == 'nt' else '' + self.assertIn(f'{on}"py.exe" "{betik}" ara "sorgu"', metin) self.assertIn(f'"py.exe" "{betik}" oku ', metin) req = {"input": [{"type": "message", "role": "user", "content": [{"type": "input_text", "text": "araştır"}]}]} self.assertTrue(internet.ekle(req, metin)) diff --git a/apps/desktop/assets/codex-bridge/test_jev_denetim.py b/apps/desktop/assets/codex-bridge/test_jev_denetim.py index 377ee98..8896de6 100644 --- a/apps/desktop/assets/codex-bridge/test_jev_denetim.py +++ b/apps/desktop/assets/codex-bridge/test_jev_denetim.py @@ -127,6 +127,7 @@ class Akis(unittest.TestCase): sahte.sor = self.sor sahte.karar = jev_denetim.karar sahte.komutlar = jev_denetim.komutlar + sahte.hazirlik_mi = jev_denetim.hazirlik_mi env = {"jev_denetim": sahte, "yarim": yarim, "time": time, "http": __import__("http.client"), "log": self.logs.append, "RESP_HOP": set(), "json": json} adlar = ("_akis_basligi", "_denetimli_akis", "_stream") @@ -216,17 +217,56 @@ class Akis(unittest.TestCase): KOMUT = (b'event: response.output_item.done\ndata: {"type": "response.output_item.done", "item": ' b'{"type": "function_call", "name": "shell", "arguments": "{\\"cmd\\": \\"dir\\"}"}}\n\n') - def test_a_step_without_text_is_asked_with_its_tool_calls(self): + def test_a_step_without_text_passes_without_jev(self): body = self.KOMUT + sse() self.calistir(body) self.assertEqual(self.sent(), body) - self.assertEqual(self.asked, ['[komut] shell: {"cmd": "dir"}']) + self.assertEqual(self.asked, []) + self.assertEqual(self.opened, []) - def test_a_blocked_step_without_text_goes_to_the_next_layer(self): + def test_tool_step_cannot_be_replaced_even_if_jev_would_refuse(self): self.puanlar = [{"ret": 0.9, "sart": 0.0}] - self.calistir(self.KOMUT + sse()) - self.assertEqual(self.opened, ["Katman-2"]) - self.assertEqual(self.sent(), self.cevap["Katman-2"]) + body = self.KOMUT + sse() + self.calistir(body) + self.assertEqual(self.opened, []) + self.assertEqual(self.asked, []) + self.assertEqual(self.sent(), body) + + def test_tool_event_reaches_client_before_upstream_finishes(self): + first = (b'data: {"type":"response.output_item.added","item":' + b'{"type":"function_call","name":"shell","arguments":""}}\n\n') + owner = self + + class Streaming(Yanit): + def __init__(self): + super().__init__(b'') + self.parts = [first, owner.KOMUT + sse()] + self.reads = 0 + + def read1(self, n): + self.reads += 1 + if self.reads == 2: + owner.assertIn(first, owner.h.wfile.getvalue()) + owner.assertEqual(owner.asked, []) + owner.assertEqual(owner.opened, []) + return super().read1(n) + + self.h._denetimli_akis(Streaming(), '[1]', None, None, 'yap', ['Katman-1'], self.ac) + self.assertEqual(self.sent(), first + self.KOMUT + sse()) + + def test_text_with_a_tool_call_is_preparation_not_a_final_refusal(self): + body = sse('Önce dosyaları inceleyeceğim.') + self.KOMUT + self.calistir(body) + self.assertEqual(self.sent(), body) + self.assertEqual(self.asked, []) + self.assertEqual(self.opened, []) + + def test_explicit_commentary_passes_without_jev(self): + body = (b'data: {"type":"response.output_item.added","item":' + b'{"type":"message","role":"assistant","phase":"commentary"}}\n\n' + sse('Araştırıyorum.')) + self.calistir(body) + self.assertEqual(self.sent(), body) + self.assertEqual(self.asked, []) def test_an_empty_step_of_the_selected_model_goes_to_the_next_layer(self): self.calistir(sse()) # no text, no tool call