fix(codex-bridge): preserve tool model and stream preparation without Jev fallback
This commit is contained in:
1 parent
8fbbe226db
commit
3726d2a553
6 files changed
+118
-14
No files matched your search
@@ -13,6 +13,7 @@ import re
|
|||||||
import sys
|
import sys
|
||||||
import urllib.parse
|
import urllib.parse
|
||||||
import urllib.request
|
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"
|
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/?"
|
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}))
|
sayfa, _ = indirici(ARAMA + urllib.parse.urlencode({"q": sorgu}))
|
||||||
sonuc = sonuclari_coz(sayfa, adet)
|
sonuc = sonuclari_coz(sayfa, adet)
|
||||||
if not sonuc:
|
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))
|
return "\n\n".join(f"{i}. {b}\n {a}\n {o}" for i, (b, a, o) in enumerate(sonuc, 1))
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -18,8 +18,8 @@ VARSAYILAN = (
|
|||||||
"Arama: {ara} \"sorgu\"\n"
|
"Arama: {ara} \"sorgu\"\n"
|
||||||
"Sayfa okuma: {oku} <adres>\n"
|
"Sayfa okuma: {oku} <adres>\n"
|
||||||
"Önce ara, sonra işe yarayan iki üç sonucu oku, cevabında kaynak adreslerini belirt. Başka bir arama aracı "
|
"Ö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 "
|
"arama (tool_search ile bulunmaz). Arama hizmeti engeli veya ayrıştırma hatasında sorguları tekrar tekrar "
|
||||||
"aramanın yapılamadığını açıkça söyle, bilgi uydurma."
|
"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 {}
|
ayar = ayar if isinstance(ayar, dict) else {}
|
||||||
if ayar.get("enabled") is False or not betik or not os.path.isfile(betik):
|
if ayar.get("enabled") is False or not betik or not os.path.isfile(betik):
|
||||||
return None
|
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")
|
return str(ayar.get("talimat") or VARSAYILAN).replace("{ara}", on + " ara").replace("{oku}", on + " oku")
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -93,6 +93,32 @@ def son_istek(req):
|
|||||||
return ""
|
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):
|
def komutlar(govde):
|
||||||
"""Yazisi olmayan adimda Jev'e giden metin: modelin bu adimda cagirdigi araclar (ad ve arguman).
|
"""Yazisi olmayan adimda Jev'e giden metin: modelin bu adimda cagirdigi araclar (ad ve arguman).
|
||||||
govde: ust kaynagin SSE yaniti (bayt). Arac cagrisi yoksa ""."""
|
govde: ust kaynagin SSE yaniti (bayt). Arac cagrisi yoksa ""."""
|
||||||
|
|||||||
@@ -901,11 +901,20 @@ class H(http.server.BaseHTTPRequestHandler):
|
|||||||
break
|
break
|
||||||
parcalar.append(chunk)
|
parcalar.append(chunk)
|
||||||
okuyan.feed(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:
|
if time.time() - son_yazi >= 10:
|
||||||
w(b": denetim\n\n"); son_yazi = time.time()
|
w(b": denetim\n\n"); son_yazi = time.time()
|
||||||
yazi = okuyan.metin.strip()
|
yazi = okuyan.metin.strip()
|
||||||
# Her adim Jev'e gider: yazi yoksa modelin bu adimda cagirdigi araclar sorulur.
|
# Araç/hazırlık yukarıda aktarıldı; burada yalnız nihai cevap denetlenir.
|
||||||
metin = yazi or jev_denetim.komutlar(b"".join(parcalar))
|
metin = yazi
|
||||||
gecmedi = False
|
gecmedi = False
|
||||||
if hata:
|
if hata:
|
||||||
gecmedi = True
|
gecmedi = True
|
||||||
|
|||||||
@@ -28,7 +28,20 @@ class AraTest(unittest.TestCase):
|
|||||||
def test_search_output_and_empty_result(self):
|
def test_search_output_and_empty_result(self):
|
||||||
metin = ara.ara("x", indirici=lambda adres: (SAYFA, "text/html"))
|
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("1. Bir başlık\n https://ornek.com/bir?a=1", metin)
|
||||||
self.assertIn("Sonuç bulunamadı", ara.ara("x", indirici=lambda adres: ("<html></html>", "text/html")))
|
self.assertIn("Sonuç bulunamadı", ara.ara("x", indirici=lambda adres: ('<div class="no-results">no results</div>', "text/html")))
|
||||||
|
|
||||||
|
def test_bot_challenge_uses_one_rss_backup_not_false_no_results(self):
|
||||||
|
calls = []
|
||||||
|
def indirici(adres):
|
||||||
|
calls.append(adres)
|
||||||
|
return (('<form id="challenge-form">Bot challenge</form>' if len(calls) == 1 else
|
||||||
|
'<rss><channel><item><title>OmniRoute</title><link>https://ornek.com/</link></item></channel></rss>'), '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: ('<html>challenge</html>', 'text/html'))
|
||||||
|
|
||||||
def test_page_is_turned_into_text_and_cut(self):
|
def test_page_is_turned_into_text_and_cut(self):
|
||||||
sayfa = "<html><head><style>a{}</style><script>x()</script></head><body><nav>menü</nav><p>Bir & iki</p><div>üç</div></body></html>"
|
sayfa = "<html><head><style>a{}</style><script>x()</script></head><body><nav>menü</nav><p>Bir & iki</p><div>üç</div></body></html>"
|
||||||
@@ -46,7 +59,8 @@ class InternetTest(unittest.TestCase):
|
|||||||
betik = os.path.join(d, "ara.py")
|
betik = os.path.join(d, "ara.py")
|
||||||
open(betik, "w").close()
|
open(betik, "w").close()
|
||||||
metin = internet.talimat(None, betik, python="py.exe")
|
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 <adres>', metin)
|
self.assertIn(f'"py.exe" "{betik}" oku <adres>', metin)
|
||||||
req = {"input": [{"type": "message", "role": "user", "content": [{"type": "input_text", "text": "araştır"}]}]}
|
req = {"input": [{"type": "message", "role": "user", "content": [{"type": "input_text", "text": "araştır"}]}]}
|
||||||
self.assertTrue(internet.ekle(req, metin))
|
self.assertTrue(internet.ekle(req, metin))
|
||||||
|
|||||||
@@ -127,6 +127,7 @@ class Akis(unittest.TestCase):
|
|||||||
sahte.sor = self.sor
|
sahte.sor = self.sor
|
||||||
sahte.karar = jev_denetim.karar
|
sahte.karar = jev_denetim.karar
|
||||||
sahte.komutlar = jev_denetim.komutlar
|
sahte.komutlar = jev_denetim.komutlar
|
||||||
|
sahte.hazirlik_mi = jev_denetim.hazirlik_mi
|
||||||
env = {"jev_denetim": sahte, "yarim": yarim, "time": time, "http": __import__("http.client"),
|
env = {"jev_denetim": sahte, "yarim": yarim, "time": time, "http": __import__("http.client"),
|
||||||
"log": self.logs.append, "RESP_HOP": set(), "json": json}
|
"log": self.logs.append, "RESP_HOP": set(), "json": json}
|
||||||
adlar = ("_akis_basligi", "_denetimli_akis", "_stream")
|
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": '
|
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')
|
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()
|
body = self.KOMUT + sse()
|
||||||
self.calistir(body)
|
self.calistir(body)
|
||||||
self.assertEqual(self.sent(), 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.puanlar = [{"ret": 0.9, "sart": 0.0}]
|
||||||
self.calistir(self.KOMUT + sse())
|
body = self.KOMUT + sse()
|
||||||
self.assertEqual(self.opened, ["Katman-2"])
|
self.calistir(body)
|
||||||
self.assertEqual(self.sent(), self.cevap["Katman-2"])
|
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):
|
def test_an_empty_step_of_the_selected_model_goes_to_the_next_layer(self):
|
||||||
self.calistir(sse()) # no text, no tool call
|
self.calistir(sse()) # no text, no tool call
|
||||||
|
|||||||
Reference in new issue
Block a user