Files
aiturk-hermes-ide/apps/desktop/assets/codex-bridge/jev_denetim.py
T
yilsemandClaude Opus 5.5 a7ba300448
Server CI and Windows package / verify (push) Successful in 3m23s
Server CI and Windows package / windows-package (push) Successful in 7m9s
fix(codex-bridge): close the gaps an independent review found
- One decision per real turn: a turn starts with a user message after the
  previous final answer; tool steps, messages typed mid-turn, a lone "."
  and compaction keep the combo. Works without prompt_cache_key too.
- The virtual Otomatik name never reaches the gateway (stage off or error
  -> last choice or targets.zor); prefilter.target may not be it.
- finishCheck: at most once per turn; a broken, empty or refused retry
  delivers the already approved first answer.
- Layer window filter: only trusted windows, never the last layer.
- Correction channel only on always-pass targets while checks are on;
  Sesli-Hizli-Zeki stays always-pass by default; a >256 KB SSE frame
  passes unfiltered instead of breaking the stream.
- Turn-start questions wait in the local queue no longer than their
  timeout; decision-turn memory under a lock with refreshed order.
- Dify fetch refuses redirects (the app key never moves host); the
  rules file write retries a Windows sharing violation.
- An unreadable settings file no longer breaks every request.
- TypeScript accepts only https:// URLs the bridge accepts;
  finishCheck.timeoutSeconds (unused) removed.

Tests: 310 bridge tests, 36 electron tests; side-bridge live run for the
turn, prefilter (incl. decision timeout) and finish-check paths.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 08:14:38 +03:00

308 lines
13 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Katman denetimi: Jev, katman combosunun her adimdaki cevabini gecirir ya da gecirmez (11 Ekim 2026).
Kullanici karari (11 Ekim 2026): "Jev kati olsun. Af yok istisna yok. Herhangi bir model Jev'e gelirse Jev bunu
gecirmezse 1.Katmana yonlendirecek. 1.Katman combosu gecmezse 2.Katmana, 2.Katman gecmezse 3.Katmana, 3.Katman
gecmezse 4.Katmana. 4.Katman combosu gelirse Jev bunu sorgulamadan gecirecek."
12 Ekim 2026 istisnasi: çözülmüş hedefi muaf listesinde olan cevap Jev'e sorulmaz (ayar: exemptModels).
Taşıma/boş yanıt kurtarması ve yedek katmanların denetimi korunur.
Diğer hedeflerde denetim secili modelin katman olup olmamasina bakmaz.
Gecmezse adimi ayardaki zincir yapar (14 Ekim 2026: Katman-1 -> Katman-2 -> Katman-3, Katman-3 muaf); secili model
zaten bir katmansa yalniz ondan sonrakiler denenir. Katmanlar OmniRoute combolaridir; kopru onlari yalniz adiyla cagirir.
14 Ekim 2026 karar asamalari (hepsi ayardan acilir, varsayilan kapali): tur basinda "Otomatik" modelin combo secimi
(autoRoute), normal modelin reddedecegi tahmin edilen isi dogrudan katmana verme (prefilter), isi bitirmeden "bitti"
diyen son cevabi ayni modele bir kez geri verme (finishCheck). Karar modeli metin yazmaz; hata ya da zaman asiminda
bugunku yol izlenir.
Jev (TypeSafe System One) metin uretmez, sorulan evet/hayir sorularina olasilik verir. Kural tektir ve istisnasi
yoktur: cevap istegi yapmiyorsa (ret) ya da sormadan kendi sartlariyla degistiriyorsa (sart) adim gecmez.
Metni olmayan (yalniz arac cagiran) adimda sorulacak cevap yoktur. Jev'e ulasilamazsa cevap oldugu gibi verilir.
"""
import json
import os
import re
import urllib.request
import jev_settings
ADRES = "https://api.typesafe.ai/v1/systemone"
MODEL = "jev-1.13.0"
KATMAN = ("Katman-1", "Katman-2", "Katman-3") # varsayilan zincir; asil zincir ayardan okunur
ESIK = {"ret": 0.5, "sart": 0.7}
SORU = {
"ret": ("The assistant did NOT do what the user asked or did NOT answer the user's question: it refused, declined, "
"said it cannot or will not, avoided giving the requested judgment, or redirected to a different topic."),
"sart": ("The assistant changed the request on its own: it added its own conditions, softened or relabeled what was "
"asked, or left part of it out, instead of doing exactly what the user asked."),
}
_ANAHTAR = {"okundu": False, "deger": ""}
def izleme_mi(hedef):
"""Bekci izleme kanali (cevaptan sonra duzeltme) bu hedefin mi? Ayar: watchModels, tam ad eslesmesi.
Jev acikken yalniz muaf hedefte: denetlenen cevap baska katmana gecebilir, makbuz kullanicinin gormedigi
cevaba baglanmasin."""
try:
s = jev_settings.read()[0]
return hedef in s['watchModels'] and (not s['enabled'] or hedef in s['exemptModels'])
except Exception:
return False
def muaf_mi(hedef):
"""Settings' always-pass list matches exact resolved model IDs.
Taşıma hatalarının katman sırasını değiştirmez; yedek model kendi adına denetlenir.
"""
return hedef in jev_settings.read()[0]['exemptModels']
def sonrakiler(hedef):
"""Model-specific rejection edges take precedence over the general fallback.
Stop at an always-pass model or before revisiting any target.
"""
settings = jev_settings.read()[0]
ad = str(hedef or "")
if not settings['enabled'] or not ad:
return []
routes = {r['from']: r['to'] for r in settings['rejectionRules']}
result, seen = [], {ad}
# An exempt model can still use the general fallback on transport failure.
# Repeated targets stop the walk even if a file was edited outside Settings.
while len(result) < 65:
target = routes.get(ad, settings['fallbackModel'])
if target in seen:
break
seen.add(target)
result.append(target)
if target in settings['exemptModels']:
break
ad = target
return result
def anahtar(ortam=None, dosyalar=None):
"""TypeSafe anahtari: ortam degiskeni, sonra ~/.typesafe/typesafe.env. Yoksa ""."""
if ortam is None and dosyalar is None:
saved = jev_settings.key()
if saved:
return saved
ortam = os.environ if ortam is None else ortam
deger = str(ortam.get("TYPESAFE_API_KEY") or "").strip()
if deger:
return deger
if dosyalar is None:
if _ANAHTAR["okundu"]:
return _ANAHTAR["deger"]
dosyalar = [os.path.join(os.path.expanduser("~"), ".typesafe", "typesafe.env")]
onbellek = True
else:
onbellek = False
for yol in dosyalar:
try:
with open(yol, encoding="utf-8", errors="replace") as f:
for satir in f:
m = re.match(r"^\s*TYPESAFE_API_KEY\s*=\s*(.*)$", satir)
if m:
deger = m.group(1).strip().strip('"').strip("'")
break
except OSError:
continue
if deger:
break
if onbellek:
_ANAHTAR.update(okundu=True, deger=deger)
return deger
def _metin(oge):
icerik = oge.get("content")
if isinstance(icerik, str):
return icerik
return "".join(str(p.get("text") or "") for p in icerik or [] if isinstance(p, dict))
def son_istek(req):
"""Kullanicinin bu turdaki istegi: son kullanici mesaji. Sesli aktarimda yalniz <input> icindeki soz;
baska sarmalli sistem mesajlari (kanca baglami) atlanir."""
return son_istek_sirasi(req)[0]
def istek_metni(oge):
"""Bir kullanici mesajindaki istek: sesli aktarimda <input> icindeki soz; sarmalli sistem mesaji ise ""."""
if not (isinstance(oge, dict) and oge.get("type", "message") == "message" and oge.get("role") == "user"):
return ""
metin = _metin(oge).strip()
m = re.search(r"<realtime_delegation>.*?<input>(.*?)</input>", metin, re.S)
if m:
return m.group(1).strip()
return metin if metin and not metin.startswith("<") else ""
def son_istek_sirasi(req):
"""(istek, girdideki sirasi). Turun arac adimlarinda da ayni sira doner; yoksa ("", -1)."""
girdi = req.get("input") or []
for sira in range(len(girdi) - 1, -1, -1):
metin = istek_metni(girdi[sira])
if metin:
return metin, sira
return "", -1
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 ""."""
satirlar = []
for satir in govde.splitlines():
if not satir.startswith(b"data:") or b"response.output_item.done" not in satir:
continue
try:
oge = json.loads(satir[5:]).get("item") or {}
except (ValueError, AttributeError):
continue
if isinstance(oge, dict) and str(oge.get("type") or "").endswith("_call"):
arguman = oge.get("arguments") or oge.get("input") or oge.get("action") or ""
if not isinstance(arguman, str):
arguman = json.dumps(arguman, ensure_ascii=False)
satirlar.append(f"[komut] {oge.get('name') or oge.get('type')}: {arguman}")
return "\n".join(satirlar)
def _gonder(govde, key, settings=None, timeout=None):
settings = settings or jev_settings.read()[0]
istek = urllib.request.Request(settings['endpoint'], data=json.dumps(govde).encode("utf-8"), method="POST",
headers={"Authorization": "Bearer " + key, "Content-Type": "application/json"})
with urllib.request.urlopen(istek, timeout=timeout or settings['timeoutSeconds']) as yanit:
return json.load(yanit)
def sorular(durum, soru, key, timeout, gonder=_gonder, settings=None):
"""Karar modeline bir cagrida birden cok evet/hayir sorusu. Donus {ad: 0-1 olasilik} | None (sorulamadi).
durum: {"user_request": ..., ["assistant_reply": ...]}; soru: {ad: talimat}."""
settings = settings or jev_settings.read()[0]
if not key or not soru or not durum.get("user_request"):
return None
govde = {"model": settings['model'], "state": durum,
"questions": {k: {"type": "noul", "instructions": v} for k, v in soru.items()}}
if not jev_settings.acquire(settings, wait=timeout):
return None
try:
response = gonder(govde, key, settings, timeout) if gonder is _gonder else gonder(govde, key)
yanitlar = (response or {}).get("answers") or {}
result = {k: float(yanitlar[k]["noul"]) for k in soru}
return result if all(0 <= v <= 1 for v in result.values()) else None
except Exception:
return None
finally:
jev_settings.release()
def on_karar(istek, oto, ret, key, gonder=_gonder):
"""Tur basi karari: istenen sorular tek cagrida. oto: Otomatik modelin combo secimi; ret: normal model reddeder mi.
Donus: {"hedef": Otomatik icin secilen ad ya da None, "ret": bool ya da None, "puan": {...} ya da None}."""
settings = jev_settings.read()[0]
a, p = settings['autoRoute'], settings['prefilter']
soru = {}
if oto:
soru.update(a['questions'])
if ret and p['enabled']:
soru["ret"] = p['question']
if not soru:
return {"hedef": None, "ret": None, "puan": None}
sure = max(a['timeoutSeconds'] if oto else 0, p['timeoutSeconds'] if ret else 0) or 3
puan = sorular({"user_request": istek}, soru, key, sure, gonder=gonder, settings=settings)
hedef = None
if oto:
if puan is None:
hedef = a['targets']['zor'] # karar alinamazsa guclu yol
elif puan["arastirma"] >= a['thresholds']['arastirma']:
hedef = a['targets']['arastirma']
elif puan["zor"] >= a['thresholds']['zor']:
hedef = a['targets']['zor']
else:
hedef = a['targets']['basit']
tahmin = None if puan is None or "ret" not in puan else puan["ret"] >= p['threshold']
return {"hedef": hedef, "ret": tahmin, "puan": puan}
def bitmedi_mi(puan):
"""finishCheck: son cevap isi bitirmeden 'bitti' mi dedi? Soru sorulmadiysa False."""
if not puan or "bitmedi" not in puan:
return False
return puan["bitmedi"] >= jev_settings.read()[0]['finishCheck']['threshold']
class Puan(dict):
"""Carry the thresholds used for this evaluation through a settings change."""
pass
def sor(istek, cevap, key, gonder=_gonder, bitis=False):
"""Jev'e sorar. Donus: {"ret", "sart"[, "bitmedi"]} olasiliklari | None (sorulamadi).
bitis=True ve finishCheck acikken ayni cagriya "is bitmeden bitti mi dedi" sorusu da eklenir."""
settings = jev_settings.read()[0]
govde = {"model": settings['model'], "state": {"user_request": istek, "assistant_reply": cevap},
"questions": {k: {"type": "noul", "instructions": v} for k, v in settings['questions'].items()}}
if bitis and settings['finishCheck']['enabled']:
govde['questions']['bitmedi'] = {"type": "noul", "instructions": settings['finishCheck']['question']}
for name, criteria in settings.get('criteria', {}).items():
values = {k: v for k, v in criteria.items() if v.strip()}
if values:
govde['questions'][name]['criteria'] = values
if not jev_settings.acquire(settings):
return None
try:
response = gonder(govde, key, settings) if gonder is _gonder else gonder(govde, key)
yanitlar = (response or {}).get("answers") or {}
result = Puan({k: float(yanitlar[k]["noul"]) for k in settings['questions']})
if 'bitmedi' in govde['questions'] and isinstance(yanitlar.get('bitmedi'), dict):
result['bitmedi'] = float(yanitlar['bitmedi']['noul']) # ek soru: cevapsizsa ana karar yine gecerli
if any(not 0 <= v <= 1 for v in result.values()):
return None
result.thresholds = settings['thresholds']
return result
except Exception:
return None
finally:
jev_settings.release()
def karar(puan):
"""True: adim gecmez, sonraki katman yapar. Istisna yok."""
thresholds = getattr(puan, 'thresholds', None) or jev_settings.read()[0]['thresholds']
return puan["ret"] >= thresholds["ret"] or puan["sart"] >= thresholds["sart"]
def hata_gecis():
return jev_settings.read()[0]['failureAction'] == 'next'
def etkin_mi():
return bool(jev_settings.read()[0]['enabled'])