feat(codex-bridge): decision-model stages, Dify rules and beta.39
Owner request, 14 October 2026: use the decision model (Jev / Celeris) at several stages, let Dify supply the rules, code it without new bugs. Every new stage is switched on in Settings > Kademeli Gecis and starts disabled, so the upgrade changes no behaviour by itself. - Settings schema 2 (Python and TypeScript validate the same rules); a schema 1 file is filled from the defaults. Default chain is now Katman-1 -> Katman-2 -> Katman-3 with Katman-3, Chat-Model and bekci-sesli-bekci always passing (owner decision of the same day). - watchModels: the Bekci correction channel follows a settings list instead of the hard-coded Sesli-Hizli-Zeki. - prefilter: a predicted refusal starts the step at the chosen layer. - autoRoute: virtual "Otomatik" model; one decision per user turn picks the combo, tool steps of the same turn keep it. - finishCheck: an answer that stops early goes back to the same model once. - A layer whose window cannot hold the request is skipped. - policy: stage rules from a Dify workflow (karar_politikasi.py), fetched in the background, validated, last good copy kept; turns never wait. - karar_sor.py: verifierCheck for the dynamic-workflow acceptance step. - Bridge version 19, IDE 1.0.0-beta.39. Tests: 299 bridge tests, 36 electron tests; live run on a side bridge (port 8797) with Celeris and the AITURK gateway for every stage. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
8f53b090f7
commit
7c4ef18df0
18 files changed
+1672
-119
No files matched your search
@@ -27,6 +27,37 @@ Run: `python bridge.py --home <data dir> --hermes-home <agent home>`. The IDE wr
|
||||
|
||||
Tests: `python -m unittest discover` in this directory.
|
||||
|
||||
## Version 19: decision stages (14 October 2026)
|
||||
|
||||
The decision model (Jev or another System One compatible service such as Celeris) can now be asked at
|
||||
several stages. Every new stage is switched on in Settings → Kademeli Geçiş and starts disabled, so an
|
||||
upgrade changes no behaviour by itself. The decision model never writes text; an unreachable or invalid
|
||||
decision always keeps the current path.
|
||||
|
||||
- `jev-settings.json` schema 2 adds `watchModels`, `prefilter`, `autoRoute`, `finishCheck`,
|
||||
`verifierCheck` and `policy`. A schema 1 file is filled from `jev-settings-defaults.json`.
|
||||
The default chain is `Katman-1 → Katman-2 → Katman-3`; `Katman-3`, `Chat-Model` and
|
||||
`bekci-sesli-bekci` always pass.
|
||||
- **Correction channel** (`watchModels`): the Bekçi watch sideband is no longer bound to the hard-coded
|
||||
name `Sesli-Hizli-Zeki`; any exact target in the list owns it.
|
||||
- **Refusal pre-check** (`prefilter`): at the start of a turn, "would a mainstream model refuse or water
|
||||
this down?". If yes, the step starts at `prefilter.target` instead of collecting a refusal first.
|
||||
- **Automatic model** (`autoRoute`): a virtual `ide/<model>` entry ("Otomatik") in the Codex list. Each new
|
||||
user message gets one decision call (hard? research?) and goes to the configured combo. Tool steps of
|
||||
the same turn keep that combo, because switching providers mid-turn breaks encrypted history. The
|
||||
user's explicit model choice is never changed by this stage.
|
||||
- Both turn-start questions go in one call, bounded by `timeoutSeconds` (default 3 s).
|
||||
- **Layer window filter**: a layer whose context window cannot hold the request is skipped.
|
||||
- **Early finish check** (`finishCheck`): asked in the same call as the refusal check on a final answer.
|
||||
If the answer stopped early or claimed success without evidence, it goes back to the same model once
|
||||
with `finishCheck.note`; the retry is delivered without another finish question.
|
||||
- **Rules from Dify** (`policy.source = dify`, `karar_politikasi.py`): the stage questions, thresholds and
|
||||
combo choices may come from a Dify workflow's `kurallar` output. Fetched in the background every
|
||||
`refreshMinutes`, validated against the local settings, last good copy kept in `jev-policy.json`.
|
||||
The transition chain and the always-pass list stay local. A turn never waits for Dify.
|
||||
- **Workflow reviewer check** (`verifierCheck`, `karar_sor.py`): the Codex dynamic-workflow acceptance
|
||||
step asks whether a reviewer approved without concrete evidence; if so acceptance is blocked.
|
||||
|
||||
## Version 11: isolated routes and cross-provider preparation
|
||||
|
||||
- Pure subscription chats and native voice retain their ChatGPT route. Added
|
||||
|
||||
@@ -35,7 +35,7 @@ import gecis_context
|
||||
import terminal_stream
|
||||
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
SURUM = "18" # AITURK IDE'nin bekledigi kopru surumu (codex-link-config.ts CODEX_BRIDGE_VERSION)
|
||||
SURUM = "19" # AITURK IDE'nin bekledigi kopru surumu (codex-link-config.ts CODEX_BRIDGE_VERSION)
|
||||
try:
|
||||
with open(os.path.abspath(__file__), "rb") as _f:
|
||||
KOD = hashlib.sha256(_f.read()).hexdigest()[:8]
|
||||
|
||||
@@ -5,10 +5,9 @@
|
||||
"fallbackModel": "Katman-1",
|
||||
"rejectionRules": [
|
||||
{ "from": "Katman-1", "to": "Katman-2" },
|
||||
{ "from": "Katman-2", "to": "Katman-3" },
|
||||
{ "from": "Katman-3", "to": "Katman-4" }
|
||||
{ "from": "Katman-2", "to": "Katman-3" }
|
||||
],
|
||||
"exemptModels": ["Sesli-Hizli-Zeki", "Katman-4"],
|
||||
"exemptModels": ["Chat-Model", "bekci-sesli-bekci", "Katman-3"],
|
||||
"thresholds": { "ret": 0.5, "sart": 0.7 },
|
||||
"questions": {
|
||||
"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.",
|
||||
@@ -18,5 +17,41 @@
|
||||
"criteria": { "ret": { "true": "", "false": "" }, "sart": { "true": "", "false": "" } },
|
||||
"maxConcurrent": 0,
|
||||
"queueWaitSeconds": 3600,
|
||||
"failureAction": "pass"
|
||||
"failureAction": "pass",
|
||||
"watchModels": ["Sesli-Hizli-Zeki", "Chat-Model", "bekci-sesli-bekci"],
|
||||
"prefilter": {
|
||||
"enabled": false,
|
||||
"question": "A mainstream AI assistant such as ChatGPT, Claude or Gemini would refuse this request, water it down, or answer it only with added warnings or conditions.",
|
||||
"threshold": 0.6,
|
||||
"target": "Katman-2",
|
||||
"timeoutSeconds": 3
|
||||
},
|
||||
"autoRoute": {
|
||||
"enabled": false,
|
||||
"model": "Otomatik",
|
||||
"timeoutSeconds": 3,
|
||||
"questions": {
|
||||
"zor": "The user's request is hard: doing it well needs multi-step work, expert or technical knowledge, careful analysis, planning, calculation or writing and changing code. Greetings, small talk, simple facts and short simple commands are NOT hard.",
|
||||
"arastirma": "The user's request is mainly research: finding, reading and comparing many sources or files and reporting what they say, rather than changing anything."
|
||||
},
|
||||
"thresholds": { "zor": 0.5, "arastirma": 0.6 },
|
||||
"targets": { "basit": "Workflow-Kesif", "zor": "Ajan", "arastirma": "Arastirma" }
|
||||
},
|
||||
"finishCheck": {
|
||||
"enabled": false,
|
||||
"question": "The assistant stopped before the user's task was finished: it said it is done or handed back without doing all of what was asked, or it claimed success (tests pass, file changed, deployed) without showing the actual evidence.",
|
||||
"threshold": 0.6,
|
||||
"note": "Önceki cevabın iş bitmeden bitti dedi ya da kanıt göstermedi. İşi gerçekten bitir; gerekiyorsa araçları kullan ve gerçek kanıtı göster.",
|
||||
"timeoutSeconds": 5
|
||||
},
|
||||
"verifierCheck": {
|
||||
"enabled": false,
|
||||
"question": "The reviewer approved the work without concrete evidence: it does not cite the actual diff, the real command output or specific lines, so the approval may be a guess.",
|
||||
"threshold": 0.6
|
||||
},
|
||||
"policy": {
|
||||
"source": "local",
|
||||
"endpoint": "https://dify.turkservis.online/v1",
|
||||
"refreshMinutes": 5
|
||||
}
|
||||
}
|
||||
@@ -4,11 +4,16 @@ Kullanici karari (11 Ekim 2026): "Jev kati olsun. Af yok istisna yok. Herhangi b
|
||||
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 tam Sesli-Hizli-Zeki olan cevap Jev'e sorulmaz.
|
||||
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 sirayla Katman-1 -> Katman-2 -> Katman-3 -> Katman-4 yapar; secili model zaten bir katmansa
|
||||
yalniz ondan sonrakiler denenir. Katmanlar OmniRoute combolaridir (KATMAN); kopru onlari yalniz adiyla cagirir.
|
||||
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.
|
||||
@@ -22,7 +27,7 @@ import jev_settings
|
||||
|
||||
ADRES = "https://api.typesafe.ai/v1/systemone"
|
||||
MODEL = "jev-1.13.0"
|
||||
KATMAN = ("Katman-1", "Katman-2", "Katman-3", "Katman-4")
|
||||
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, "
|
||||
@@ -32,6 +37,14 @@ SORU = {
|
||||
}
|
||||
_ANAHTAR = {"okundu": False, "deger": ""}
|
||||
|
||||
def izleme_mi(hedef):
|
||||
"""Bekci izleme kanali (cevaptan sonra duzeltme) bu hedefin mi? Ayar: watchModels, tam ad eslesmesi."""
|
||||
try:
|
||||
return hedef in jev_settings.read()[0]['watchModels']
|
||||
except Exception:
|
||||
return hedef == "Sesli-Hizli-Zeki"
|
||||
|
||||
|
||||
def muaf_mi(hedef):
|
||||
"""Settings' always-pass list matches exact resolved model IDs.
|
||||
|
||||
@@ -109,16 +122,23 @@ def _metin(oge):
|
||||
def son_istek(req):
|
||||
"""Kullanicinin bu turdaki istegi: son kullanici mesaji. Sesli aktarimda yalniz <input> icindeki soz;
|
||||
baska sarmalli sistem mesajlari (kanca baglami) atlanir."""
|
||||
for oge in reversed(req.get("input") or []):
|
||||
return son_istek_sirasi(req)[0]
|
||||
|
||||
|
||||
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):
|
||||
oge = girdi[sira]
|
||||
if not (isinstance(oge, dict) and oge.get("type", "message") == "message" and oge.get("role") == "user"):
|
||||
continue
|
||||
metin = _metin(oge).strip()
|
||||
m = re.search(r"<realtime_delegation>.*?<input>(.*?)</input>", metin, re.S)
|
||||
if m:
|
||||
return m.group(1).strip()
|
||||
return m.group(1).strip(), sira
|
||||
if metin and not metin.startswith("<"):
|
||||
return metin
|
||||
return ""
|
||||
return metin, sira
|
||||
return "", -1
|
||||
|
||||
|
||||
def hazirlik_mi(govde):
|
||||
@@ -166,24 +186,83 @@ def komutlar(govde):
|
||||
return "\n".join(satirlar)
|
||||
|
||||
|
||||
def _gonder(govde, key, settings=None):
|
||||
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=settings['timeoutSeconds']) as yanit:
|
||||
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):
|
||||
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):
|
||||
"""Jev'e sorar. Donus: {"ret", "sart"} olasiliklari | None (sorulamadi)."""
|
||||
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:
|
||||
@@ -193,7 +272,9 @@ def sor(istek, cevap, key, gonder=_gonder):
|
||||
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 SORU})
|
||||
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']
|
||||
|
||||
@@ -1,4 +1,11 @@
|
||||
"""Per-user settings, re-read for new evaluations without restarting active chats."""
|
||||
"""Per-user settings, re-read for new evaluations without restarting active chats.
|
||||
|
||||
Schema 2 (14 Oct 2026) adds the decision stages: watchModels, prefilter, autoRoute, finishCheck,
|
||||
verifierCheck and policy. A schema 1 file stays valid: every missing section is filled from the
|
||||
defaults, and every new stage starts disabled, so an upgrade changes no behaviour by itself.
|
||||
With policy.source "dify" the stage settings may be overridden by the last good rules fetched
|
||||
from Dify (jev-policy.json); invalid or missing rules leave the local settings in force.
|
||||
"""
|
||||
import base64
|
||||
import copy
|
||||
import ctypes
|
||||
@@ -8,8 +15,13 @@ import sys
|
||||
import threading
|
||||
import time
|
||||
from pathlib import Path
|
||||
from urllib.parse import urlsplit
|
||||
|
||||
DEFAULTS = json.loads(Path(__file__).with_name('jev-settings-defaults.json').read_text(encoding='utf-8'))
|
||||
POLICY_FILE = 'jev-policy.json'
|
||||
# Sections Dify rules may change. The transition chain and the always-pass list stay local.
|
||||
POLICY_KEYS = ('prefilter', 'autoRoute', 'finishCheck', 'verifierCheck', 'thresholds', 'questions')
|
||||
SECTIONS = ('watchModels', 'prefilter', 'autoRoute', 'finishCheck', 'verifierCheck', 'policy')
|
||||
_condition = threading.Condition()
|
||||
_active = 0
|
||||
|
||||
@@ -20,16 +32,37 @@ def home():
|
||||
return Path(os.environ.get('AITURK_CODEX_BRIDGE_HOME') or Path(__file__).parent)
|
||||
|
||||
|
||||
def read():
|
||||
try:
|
||||
data = json.loads((home() / 'jev-settings.json').read_text(encoding='utf-8'))
|
||||
except FileNotFoundError:
|
||||
return copy.deepcopy(DEFAULTS), None
|
||||
settings = data['settings']
|
||||
def _fill(value, default):
|
||||
"""Missing keys of a schema 1 file come from the defaults; present values are kept as written."""
|
||||
if isinstance(default, dict) and isinstance(value, dict):
|
||||
return {k: _fill(value[k], d) if k in value else copy.deepcopy(d) for k, d in default.items()} | \
|
||||
{k: v for k, v in value.items() if k not in default}
|
||||
return value
|
||||
|
||||
|
||||
def _text(value, limit=10000):
|
||||
return isinstance(value, str) and bool(value.strip()) and len(value) <= limit
|
||||
|
||||
|
||||
def _prob(value):
|
||||
return isinstance(value, (int, float)) and not isinstance(value, bool) and 0 <= value <= 1
|
||||
|
||||
|
||||
def _int(value, low, high):
|
||||
return isinstance(value, int) and not isinstance(value, bool) and low <= value <= high
|
||||
|
||||
|
||||
def _https(value):
|
||||
if not _text(value, 2048):
|
||||
return False
|
||||
url = urlsplit(value)
|
||||
return url.scheme == 'https' and bool(url.netloc) and not url.username and not url.password
|
||||
|
||||
|
||||
def validate(settings):
|
||||
"""Raise ValueError when a value could route to the wrong place or wait without bound."""
|
||||
# A corrupted file must not silently change routing or contact another host.
|
||||
from urllib.parse import urlsplit
|
||||
url = urlsplit(settings['endpoint'])
|
||||
if url.scheme != 'https' or not url.netloc or url.username or url.password:
|
||||
if not _https(settings['endpoint']):
|
||||
raise ValueError('Invalid Jev endpoint')
|
||||
for name in ('ret', 'sart'):
|
||||
n = settings['thresholds'][name]
|
||||
@@ -42,24 +75,79 @@ def read():
|
||||
raise ValueError('Invalid Jev concurrency')
|
||||
if not settings['fallbackModel'] or not isinstance(settings['rejectionRules'], list) or settings['failureAction'] not in ('pass', 'next'):
|
||||
raise ValueError('Invalid Jev routing')
|
||||
return settings, data.get('protectedKey')
|
||||
watch = settings['watchModels']
|
||||
if not isinstance(watch, list) or len(watch) > 64 or not all(_text(x, 256) for x in watch) or len(set(watch)) != len(watch):
|
||||
raise ValueError('Invalid watch models')
|
||||
p = settings['prefilter']
|
||||
if not (isinstance(p.get('enabled'), bool) and _text(p.get('question')) and _prob(p.get('threshold'))
|
||||
and _text(p.get('target'), 256) and _int(p.get('timeoutSeconds'), 1, 60)):
|
||||
raise ValueError('Invalid prefilter')
|
||||
a = settings['autoRoute']
|
||||
if not (isinstance(a.get('enabled'), bool) and _text(a.get('model'), 256) and _int(a.get('timeoutSeconds'), 1, 60)
|
||||
and all(_text((a.get('questions') or {}).get(k)) and _prob((a.get('thresholds') or {}).get(k)) for k in ('zor', 'arastirma'))
|
||||
and all(_text((a.get('targets') or {}).get(k), 256) for k in ('basit', 'zor', 'arastirma'))):
|
||||
raise ValueError('Invalid automatic routing')
|
||||
if a['model'] in a['targets'].values():
|
||||
raise ValueError('Automatic routing cannot target itself')
|
||||
f = settings['finishCheck']
|
||||
if not (isinstance(f.get('enabled'), bool) and _text(f.get('question')) and _prob(f.get('threshold'))
|
||||
and _text(f.get('note'), 4000) and _int(f.get('timeoutSeconds'), 1, 60)):
|
||||
raise ValueError('Invalid finish check')
|
||||
v = settings['verifierCheck']
|
||||
if not (isinstance(v.get('enabled'), bool) and _text(v.get('question')) and _prob(v.get('threshold'))):
|
||||
raise ValueError('Invalid verifier check')
|
||||
pol = settings['policy']
|
||||
if not (pol.get('source') in ('local', 'dify') and _https(pol.get('endpoint')) and _int(pol.get('refreshMinutes'), 1, 1440)):
|
||||
raise ValueError('Invalid policy source')
|
||||
return settings
|
||||
|
||||
|
||||
def key():
|
||||
_, encrypted = read()
|
||||
def _policy(settings):
|
||||
"""Last good Dify rules on top of the local settings; anything invalid keeps the local settings."""
|
||||
if settings['policy']['source'] != 'dify':
|
||||
return settings
|
||||
try:
|
||||
cache = json.loads((home() / POLICY_FILE).read_text(encoding='utf-8'))
|
||||
overrides = cache.get('overrides')
|
||||
if not cache.get('ok') or not isinstance(overrides, dict):
|
||||
return settings
|
||||
merged = copy.deepcopy(settings)
|
||||
for key in POLICY_KEYS:
|
||||
if isinstance(overrides.get(key), dict):
|
||||
merged[key] = _fill(overrides[key], settings[key])
|
||||
return validate(merged)
|
||||
except (OSError, ValueError, KeyError, TypeError, AttributeError):
|
||||
return settings
|
||||
|
||||
|
||||
def read(overlay=True):
|
||||
"""(settings, protectedKey). overlay=False: the local settings without the Dify rules."""
|
||||
try:
|
||||
data = json.loads((home() / 'jev-settings.json').read_text(encoding='utf-8'))
|
||||
except FileNotFoundError:
|
||||
return copy.deepcopy(DEFAULTS), None
|
||||
settings = data['settings']
|
||||
for section in SECTIONS:
|
||||
settings[section] = _fill(settings[section], DEFAULTS[section]) if section in settings else copy.deepcopy(DEFAULTS[section])
|
||||
validate(settings)
|
||||
return (_policy(settings) if overlay else settings), data.get('protectedKey')
|
||||
|
||||
|
||||
class _Blob(ctypes.Structure):
|
||||
_fields_ = [('cbData', ctypes.c_uint32), ('pbData', ctypes.POINTER(ctypes.c_ubyte))]
|
||||
|
||||
|
||||
def _unprotect(encrypted):
|
||||
if not encrypted:
|
||||
return ''
|
||||
if os.name != 'nt':
|
||||
raise RuntimeError('Windows credential protection required')
|
||||
from ctypes import wintypes
|
||||
class Blob(ctypes.Structure):
|
||||
_fields_ = [('cbData', wintypes.DWORD), ('pbData', ctypes.POINTER(ctypes.c_ubyte))]
|
||||
raw = base64.b64decode(encrypted, validate=True)
|
||||
buf = (ctypes.c_ubyte * len(raw)).from_buffer_copy(raw)
|
||||
source, result = Blob(len(raw), buf), Blob()
|
||||
source, result = _Blob(len(raw), buf), _Blob()
|
||||
decrypt = ctypes.windll.crypt32.CryptUnprotectData
|
||||
decrypt.argtypes = [ctypes.POINTER(Blob), ctypes.c_void_p, ctypes.c_void_p, ctypes.c_void_p, ctypes.c_void_p, wintypes.DWORD, ctypes.POINTER(Blob)]
|
||||
decrypt.restype = wintypes.BOOL
|
||||
decrypt.argtypes = [ctypes.POINTER(_Blob), ctypes.c_void_p, ctypes.c_void_p, ctypes.c_void_p, ctypes.c_void_p, ctypes.c_uint32, ctypes.POINTER(_Blob)]
|
||||
decrypt.restype = ctypes.c_int
|
||||
if not decrypt(ctypes.byref(source), None, None, None, None, 0, ctypes.byref(result)):
|
||||
raise ctypes.WinError()
|
||||
try:
|
||||
@@ -69,6 +157,41 @@ def key():
|
||||
ctypes.windll.kernel32.LocalFree(result.pbData)
|
||||
|
||||
|
||||
def key():
|
||||
_, encrypted = read()
|
||||
return _unprotect(encrypted)
|
||||
|
||||
|
||||
def policy_key():
|
||||
"""Dify app key for the rules source (protectedPolicyKey, Windows DPAPI). Empty when not set."""
|
||||
try:
|
||||
data = json.loads((home() / 'jev-settings.json').read_text(encoding='utf-8'))
|
||||
except (OSError, ValueError):
|
||||
return ''
|
||||
return _unprotect(data.get('protectedPolicyKey'))
|
||||
|
||||
|
||||
def write_policy(result):
|
||||
"""Atomically store the outcome of a Dify fetch. A failed fetch keeps the last good overrides."""
|
||||
path = home() / POLICY_FILE
|
||||
previous = {}
|
||||
try:
|
||||
previous = json.loads(path.read_text(encoding='utf-8'))
|
||||
except (OSError, ValueError):
|
||||
pass
|
||||
record = {'fetchedAt': time.strftime('%Y-%m-%dT%H:%M:%S%z'), 'ok': bool(result.get('ok')), 'error': str(result.get('error') or '')[:500]}
|
||||
record['overrides'] = result.get('overrides') if result.get('ok') else previous.get('overrides')
|
||||
if not result.get('ok') and previous.get('ok'):
|
||||
record['ok'] = True # the last good rules stay in force; the error is reported beside them
|
||||
record['lastGoodAt'] = previous.get('lastGoodAt') or previous.get('fetchedAt')
|
||||
elif result.get('ok'):
|
||||
record['lastGoodAt'] = record['fetchedAt']
|
||||
tmp = path.with_name(path.name + f'.{os.getpid()}.tmp')
|
||||
tmp.write_text(json.dumps(record, ensure_ascii=False, indent=2), encoding='utf-8')
|
||||
os.replace(tmp, path)
|
||||
return record
|
||||
|
||||
|
||||
def acquire(settings):
|
||||
global _active
|
||||
deadline = time.monotonic() + settings['queueWaitSeconds']
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
"""Decision rules from Dify (14 Oct 2026).
|
||||
|
||||
Owner request, 14 Oct 2026: "Dify'de hazirlansa oradan gelse". The model a Codex step talks to stays an
|
||||
OmniRoute combo, because a Dify app would not pass Codex's own tools through. What Dify supplies is the rules:
|
||||
the decision questions, thresholds and which combo each kind of request goes to. The bridge fetches them in
|
||||
the background (policy.refreshMinutes), validates them against the local settings and stores the last good
|
||||
copy in jev-policy.json. A turn never waits for Dify; a failed fetch keeps the last good rules.
|
||||
|
||||
The Dify workflow returns one output named ``kurallar``: a JSON object (or JSON text) whose keys are any of
|
||||
jev_settings.POLICY_KEYS, each holding the same fields as the local settings section, for example
|
||||
{"autoRoute": {"enabled": true, "targets": {"zor": "Ajan"}}}.
|
||||
"""
|
||||
import copy
|
||||
import json
|
||||
import time
|
||||
import urllib.request
|
||||
|
||||
import jev_settings
|
||||
|
||||
UA = "AITURK-IDE-codex-bridge/karar-politikasi"
|
||||
MAX_BYTES = 64 * 1024
|
||||
_son = {"zaman": 0.0}
|
||||
|
||||
|
||||
def ayikla(cikti, yerel):
|
||||
"""Dify output -> validated overrides. Raises ValueError with a short reason when unusable."""
|
||||
if isinstance(cikti, str):
|
||||
if len(cikti.encode("utf-8")) > MAX_BYTES:
|
||||
raise ValueError("kurallar too large")
|
||||
cikti = json.loads(cikti)
|
||||
if not isinstance(cikti, dict):
|
||||
raise ValueError("kurallar is not an object")
|
||||
bilinmeyen = sorted(set(cikti) - set(jev_settings.POLICY_KEYS))
|
||||
if bilinmeyen:
|
||||
raise ValueError("unknown sections: " + ", ".join(bilinmeyen))
|
||||
overrides = {k: v for k, v in cikti.items() if isinstance(v, dict)}
|
||||
if len(overrides) != len(cikti):
|
||||
raise ValueError("every section must be an object")
|
||||
birlesik = copy.deepcopy(yerel)
|
||||
for k, v in overrides.items():
|
||||
birlesik[k] = jev_settings._fill(v, yerel[k])
|
||||
jev_settings.validate(birlesik)
|
||||
return overrides
|
||||
|
||||
|
||||
def cek(settings, key, urlopen=urllib.request.urlopen):
|
||||
"""One fetch. Returns {"ok": bool, "overrides": {...}, "error": str}; never raises."""
|
||||
if not key:
|
||||
return {"ok": False, "error": "Dify key not configured"}
|
||||
url = settings['policy']['endpoint'].rstrip('/') + '/workflows/run'
|
||||
govde = json.dumps({"inputs": {}, "response_mode": "blocking", "user": "aiturk-codex-bridge"}).encode("utf-8")
|
||||
istek = urllib.request.Request(url, data=govde, method="POST", headers={
|
||||
"Authorization": "Bearer " + key, "Content-Type": "application/json", "User-Agent": UA})
|
||||
try:
|
||||
with urlopen(istek, timeout=30) as yanit:
|
||||
ham = yanit.read(MAX_BYTES * 4 + 1)
|
||||
if len(ham) > MAX_BYTES * 4:
|
||||
return {"ok": False, "error": "Dify response too large"}
|
||||
veri = json.loads(ham)
|
||||
cikti = ((veri.get("data") or {}).get("outputs") or {}).get("kurallar")
|
||||
if cikti is None:
|
||||
return {"ok": False, "error": "Dify output 'kurallar' missing"}
|
||||
return {"ok": True, "overrides": ayikla(cikti, settings)}
|
||||
except Exception as e:
|
||||
return {"ok": False, "error": f"{type(e).__name__}: {e}"[:300]}
|
||||
|
||||
|
||||
def zamani_mi(settings, simdi=None):
|
||||
simdi = time.time() if simdi is None else simdi
|
||||
return settings['policy']['source'] == 'dify' and simdi - _son["zaman"] >= settings['policy']['refreshMinutes'] * 60
|
||||
|
||||
|
||||
def yenile(log=lambda _m: None, urlopen=urllib.request.urlopen, zorla=False):
|
||||
"""Fetch when due; store the outcome. Local settings are read without the Dify overlay."""
|
||||
try:
|
||||
yerel, _ = _yerel_ayar()
|
||||
except Exception as e:
|
||||
log(f"karar kurallari: ayar okunamadi ({type(e).__name__})")
|
||||
return None
|
||||
if not zorla and not zamani_mi(yerel):
|
||||
return None
|
||||
_son["zaman"] = time.time()
|
||||
sonuc = cek(yerel, jev_settings.policy_key(), urlopen=urlopen)
|
||||
kayit = jev_settings.write_policy(sonuc)
|
||||
log("karar kurallari Dify'dan alindi" if sonuc.get("ok") else f"karar kurallari alinamadi: {sonuc.get('error')}"
|
||||
+ (" (son gecerli kurallar kullaniliyor)" if kayit.get("ok") else ""))
|
||||
return kayit
|
||||
|
||||
|
||||
def _yerel_ayar():
|
||||
"""Settings as written locally (no Dify overlay), so a bad overlay can never validate itself."""
|
||||
return jev_settings.read(overlay=False)
|
||||
@@ -0,0 +1,58 @@
|
||||
"""One decision-model question from the command line (14 Oct 2026).
|
||||
|
||||
Used by the Codex dynamic-workflow acceptance step (verifierCheck): before a reviewer's "kabul" is accepted,
|
||||
the decision model is asked whether the approval cites concrete evidence. Reads the IDE's own Kademeli Geçiş
|
||||
settings and key; nothing is written.
|
||||
|
||||
python karar_sor.py dogrulayici < {"user_request": "...", "assistant_reply": "..."}
|
||||
|
||||
Prints JSON {"enabled": bool, "kanitsiz": bool|null, "puan": float|null, "neden": str}. Exit 0 also when the
|
||||
stage is off or the model cannot be reached; the caller decides what an unanswered check means.
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||||
|
||||
import jev_denetim # noqa: E402
|
||||
import jev_settings # noqa: E402
|
||||
|
||||
SINIR = 20000
|
||||
|
||||
|
||||
def dogrulayici(veri, anahtar=None, gonder=None):
|
||||
s = jev_settings.read()[0]
|
||||
v = s['verifierCheck']
|
||||
if not (s['enabled'] and v['enabled']):
|
||||
return {'enabled': False, 'kanitsiz': None, 'puan': None, 'neden': 'kapali'}
|
||||
durum = {'user_request': str(veri.get('user_request') or '')[:SINIR], 'assistant_reply': str(veri.get('assistant_reply') or '')[:SINIR]}
|
||||
ek = {'gonder': gonder} if gonder else {}
|
||||
puan = jev_denetim.sorular(durum, {'kanitsiz': v['question']}, jev_denetim.anahtar() if anahtar is None else anahtar, 15, **ek)
|
||||
if puan is None:
|
||||
return {'enabled': True, 'kanitsiz': None, 'puan': None, 'neden': 'karar modeline sorulamadi'}
|
||||
return {'enabled': True, 'kanitsiz': puan['kanitsiz'] >= v['threshold'], 'puan': round(puan['kanitsiz'], 3), 'neden': ''}
|
||||
|
||||
|
||||
def main(argv=None, stdin=None, out=None):
|
||||
argv = sys.argv[1:] if argv is None else argv
|
||||
stdin, out = stdin or sys.stdin, out or sys.stdout
|
||||
soru = [a for i, a in enumerate(argv) if a != '--home' and (i == 0 or argv[i - 1] != '--home')]
|
||||
if soru != ['dogrulayici']:
|
||||
print('kullanim: python karar_sor.py dogrulayici < {"user_request": ..., "assistant_reply": ...}', file=sys.stderr)
|
||||
return 2
|
||||
try:
|
||||
veri = json.load(stdin)
|
||||
sonuc = dogrulayici(veri if isinstance(veri, dict) else {})
|
||||
except Exception as e:
|
||||
sonuc = {'enabled': None, 'kanitsiz': None, 'puan': None, 'neden': f'{type(e).__name__}: {e}'[:300]}
|
||||
out.write(json.dumps(sonuc, ensure_ascii=False))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
# Installed copy: the settings live in the user's bridge home, not next to this file.
|
||||
if '--home' not in sys.argv and not os.environ.get('AITURK_CODEX_BRIDGE_HOME'):
|
||||
os.environ['AITURK_CODEX_BRIDGE_HOME'] = str(Path(os.environ.get('LOCALAPPDATA', '')) / 'TurkServis' / 'AITURK-IDE' / 'codex-bridge')
|
||||
sys.exit(main())
|
||||
@@ -34,13 +34,15 @@ import model_gecis
|
||||
import is_hatti
|
||||
import internet
|
||||
import jev_denetim
|
||||
import jev_settings
|
||||
import karar_politikasi
|
||||
|
||||
from bekci_watch import MetadataFilter
|
||||
|
||||
# Installed only by owned native launcher, never an HTTP control endpoint.
|
||||
WATCH_OWNER = None
|
||||
|
||||
SURUM = "18"
|
||||
SURUM = "19"
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
try:
|
||||
with open(os.path.abspath(__file__), "rb") as _f:
|
||||
@@ -228,6 +230,22 @@ def load_ide_catalog(ag=True):
|
||||
"kaynak": g.get("pencere_kaynak") or "", "claude": bool(g.get("claude"))}
|
||||
model_map[slug] = (IDE_ROUTE, g["id"], None)
|
||||
n += 1
|
||||
oto = _oto_ayari()
|
||||
if oto and IDE_PREFIX + oto["model"] not in model_map:
|
||||
# Sanal model: istek geldiginde karar modeli ayardaki combo'lardan birini secer (14 Ekim 2026).
|
||||
slug = IDE_PREFIX + oto["model"]
|
||||
pencereler = [IDE_BILGI.get(IDE_PREFIX + h, {}).get("pencere") for h in set(oto["targets"].values())]
|
||||
pencere_oto = min(pencereler) if pencereler and all(pencereler) else None
|
||||
extra_models.append({"slug": slug, "display_name": f"{oto['model']} (karar modeli seçer)",
|
||||
"description": "Her turda karar modeli işin türüne göre combo seçer: "
|
||||
+ ", ".join(f"{k}: {v}" for k, v in oto["targets"].items()),
|
||||
"route": IDE_ROUTE, "target": oto["model"], "clone_from": clone, "priority": 299,
|
||||
"reasoning_levels": levels, "kind": "combo", "context_window": pencere_oto})
|
||||
IDE_BILGI[slug] = {"pencere": pencere_oto, "gorsel": all(IDE_BILGI.get(IDE_PREFIX + h, {}).get("gorsel")
|
||||
for h in set(oto["targets"].values())),
|
||||
"arac": True, "kaynak": "", "claude": False}
|
||||
model_map[slug] = (IDE_ROUTE, oto["model"], None)
|
||||
n += 1
|
||||
else:
|
||||
ROUTES.pop(IDE_ROUTE, None)
|
||||
MODEL_MAP = model_map
|
||||
@@ -237,10 +255,94 @@ def load_ide_catalog(ag=True):
|
||||
IDE_DURUM[0] = ozet
|
||||
log(f"AITURK IDE Codex Baglama: {ozet}")
|
||||
|
||||
def _oto_ayari():
|
||||
"""autoRoute acik ve Kademeli Gecis acikken onun ayari; degilse None. Ayar okunamazsa None."""
|
||||
try:
|
||||
s = jev_settings.read()[0]
|
||||
except Exception:
|
||||
return None
|
||||
return s["autoRoute"] if s["enabled"] and s["autoRoute"]["enabled"] else None
|
||||
|
||||
# Tur basi kararlari: sohbet -> (tur anahtari, gelen slug, secilen slug). Ayni turun arac adimlari ayni karari kullanir.
|
||||
ON_KARAR = {}
|
||||
ON_KARAR_SINIR = 500
|
||||
|
||||
# Acilista pencere verisi icin aga cikilmaz (IDE koprunun dinlemeye baslamasini sinirli sure bekler);
|
||||
# ilk tam yenileme asagidaki dongunun basinda, arka planda yapilir.
|
||||
load_ide_catalog(ag=False)
|
||||
|
||||
def karar_asamasi(n, req, sohbet, slug):
|
||||
"""14 Ekim 2026 karar asamalari: "Otomatik" modelin combo secimi (autoRoute) ve normal modelin reddedecegi
|
||||
tahmin edilen isi dogrudan ayardaki katmana verme (prefilter). Karar tur basinda bir kez verilir; ayni turun
|
||||
arac adimlari ayni karari kullanir (her adimda model degisirse saglayicilar arasi gecmis bozulur). Kapali ayar,
|
||||
bos istek ya da karar modeli hatasi bugunku yolu degistirmez (Otomatik icin guclu hedef secilir)."""
|
||||
if not slug.startswith(IDE_PREFIX):
|
||||
return slug
|
||||
settings = jev_settings.read()[0]
|
||||
if not settings["enabled"]:
|
||||
return slug
|
||||
a, p = settings["autoRoute"], settings["prefilter"]
|
||||
oto = a["enabled"] and slug == IDE_PREFIX + a["model"]
|
||||
hedef = slug[len(IDE_PREFIX):]
|
||||
ret = p["enabled"] and (oto or not (jev_denetim.muaf_mi(hedef) or hedef == p["target"]
|
||||
or hedef in jev_denetim.sonrakiler(p["target"])))
|
||||
if not oto and not ret:
|
||||
return slug
|
||||
istek, sira = jev_denetim.son_istek_sirasi(req)
|
||||
# Tur = son kullanici mesaji (metni ve girdideki yeri); ayni turun arac adimlarinda degismez.
|
||||
tur = hashlib.sha256(f"{sohbet}\0{sira}\0{istek}".encode("utf-8", "replace")).hexdigest()
|
||||
kayit = ON_KARAR.get(sohbet) if sohbet else None
|
||||
if kayit and kayit[0] == tur and kayit[1] == slug:
|
||||
secilen = kayit[2]
|
||||
else:
|
||||
if istek:
|
||||
t0 = time.time()
|
||||
sonuc = jev_denetim.on_karar(istek, oto, ret, jev_denetim.anahtar())
|
||||
puan = sonuc["puan"]
|
||||
log(f"[{n} {slug}] tur basi karari ({time.time() - t0:.1f} sn): "
|
||||
+ (", ".join(f"{k}={v:.2f}" for k, v in puan.items()) if puan else "karar modeline sorulamadi"))
|
||||
else:
|
||||
sonuc = {"hedef": a["targets"]["zor"] if oto else None, "ret": None}
|
||||
if oto:
|
||||
hedef = sonuc["hedef"]
|
||||
secilen = IDE_PREFIX + hedef
|
||||
if (p["enabled"] and sonuc["ret"] and not jev_denetim.muaf_mi(hedef) and hedef != p["target"]
|
||||
and hedef not in jev_denetim.sonrakiler(p["target"])):
|
||||
log(f"[{n} {slug}] ret on kontrolu: normal model reddedebilir, adim {p['target']} katmanindan basliyor")
|
||||
secilen = IDE_PREFIX + p["target"]
|
||||
if sohbet:
|
||||
ON_KARAR[sohbet] = (tur, slug, secilen)
|
||||
while len(ON_KARAR) > ON_KARAR_SINIR:
|
||||
ON_KARAR.pop(next(iter(ON_KARAR)))
|
||||
if secilen != slug:
|
||||
log(f"[{n} {slug}] karar: bu tur {secilen} modeline gidiyor")
|
||||
req["model"] = secilen
|
||||
return secilen
|
||||
|
||||
def sigan_katmanlar(sira, req, tag):
|
||||
"""Istegi sigdiramayacak katman zincirden cikar (bos baglam hatasiyla bosa dusmesin)."""
|
||||
tahmin, sonuc = None, []
|
||||
for ad in sira:
|
||||
p = (IDE_BILGI.get(IDE_PREFIX + ad) or {}).get("pencere")
|
||||
if p:
|
||||
if tahmin is None:
|
||||
tahmin = pencere.tahmini_token(req, 2.0)
|
||||
if tahmin > int(p * 0.90):
|
||||
log(f"{tag} katman {ad} atlandi: istek sigmaz (tahmini {tahmin} token, pencere {p})")
|
||||
continue
|
||||
sonuc.append(ad)
|
||||
return sonuc
|
||||
|
||||
def karar_kurallari_dongusu():
|
||||
"""Dify kural kaynagi (policy.source = dify) arka planda yenilenir; tur hic beklemez."""
|
||||
time.sleep(5)
|
||||
while True:
|
||||
try:
|
||||
karar_politikasi.yenile(log)
|
||||
except Exception as e:
|
||||
log(f"karar kurallari yenileme hatasi: {type(e).__name__}: {e}")
|
||||
time.sleep(60)
|
||||
|
||||
def catalog_refresh_loop():
|
||||
"""Kataloglari arka planda yeniler; liste istegini yavaslatmaz."""
|
||||
minutes = max(1, int(CFG.get("catalog_refresh_min", 5)))
|
||||
@@ -258,6 +360,7 @@ def catalog_refresh_loop():
|
||||
log(f"katalog yenileme hatasi: {e}")
|
||||
|
||||
threading.Thread(target=catalog_refresh_loop, daemon=True).start()
|
||||
threading.Thread(target=karar_kurallari_dongusu, daemon=True).start()
|
||||
|
||||
# ---- AITURK is hatti: isi gonderebilen AITURK komutu (is_hatti.py). Kurulum sonradan gelebilir: yoksa 2 dk'da bir bakilir.
|
||||
HAT_KOMUT = [None]
|
||||
@@ -943,7 +1046,7 @@ class H(http.server.BaseHTTPRequestHandler):
|
||||
self.send_header("Transfer-Encoding", "chunked")
|
||||
self.end_headers()
|
||||
|
||||
def _denetimli_akis(self, resp, tag, rewriter, izle, istek, sira, ac, hedef=None):
|
||||
def _denetimli_akis(self, resp, tag, rewriter, izle, istek, sira, ac, hedef=None, devam=None):
|
||||
"""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."""
|
||||
@@ -955,6 +1058,7 @@ class H(http.server.BaseHTTPRequestHandler):
|
||||
else:
|
||||
self._akis_basligi(resp, resp.status)
|
||||
kalan, son_yazi, son_govde = list(sira), time.time(), b""
|
||||
devam_kullanildi = False # finishCheck: son cevap ayni modele en fazla bir kez geri verilir
|
||||
|
||||
def w(b):
|
||||
if b:
|
||||
@@ -999,7 +1103,10 @@ class H(http.server.BaseHTTPRequestHandler):
|
||||
jev_done, jev_result = jev_threading.Event(), [None]
|
||||
def denetle():
|
||||
try:
|
||||
jev_result[0] = jev_denetim.sor(istek, metin, jev_denetim.anahtar())
|
||||
if devam is not None and not devam_kullanildi:
|
||||
jev_result[0] = jev_denetim.sor(istek, metin, jev_denetim.anahtar(), bitis=True)
|
||||
else:
|
||||
jev_result[0] = jev_denetim.sor(istek, metin, jev_denetim.anahtar())
|
||||
finally:
|
||||
jev_done.set()
|
||||
jev_threading.Thread(target=denetle, daemon=True).start()
|
||||
@@ -1011,8 +1118,20 @@ class H(http.server.BaseHTTPRequestHandler):
|
||||
log(f"{tag} katman denetimi: Jev'e sorulamadi, {'siradaki katman' if gecmedi else 'cevap oldugu gibi geciyor'}")
|
||||
else:
|
||||
gecmedi = jev_denetim.karar(puan)
|
||||
log(f"{tag} katman denetimi: ret={puan['ret']:.2f} sart={puan['sart']:.2f} -> "
|
||||
log(f"{tag} katman denetimi: ret={puan['ret']:.2f} sart={puan['sart']:.2f}"
|
||||
+ (f" bitmedi={puan['bitmedi']:.2f}" if 'bitmedi' in puan else "") + " -> "
|
||||
f"{'GECMEDI' if gecmedi else 'gecti'} ({time.time() - t0:.1f} sn, {len(metin)} karakter{'' if yazi else ', yalniz komut'})")
|
||||
if not gecmedi and devam is not None and not devam_kullanildi and jev_denetim.bitmedi_mi(puan):
|
||||
devam_kullanildi = True
|
||||
try:
|
||||
yeni_devam = devam(hedef, metin)
|
||||
except Exception as e:
|
||||
yeni_devam = None
|
||||
log(f"{tag} bitis denetimi: devam istegi acilamadi, cevap oldugu gibi geciyor ({type(e).__name__}: {e})")
|
||||
if yeni_devam is not None:
|
||||
resp, rewriter, tag = yeni_devam
|
||||
log(f"{tag} bitis denetimi: is bitmeden bitti denmis, ayni modele bir kez geri verildi")
|
||||
continue
|
||||
else:
|
||||
# Bos adim: ne yazi var ne arac cagrisi (hata olayi da boyledir, or. "upstream 400"). Kabul
|
||||
# edilmez, siradaki katman yapar; bos yanit cevap diye verilirse Codex adimi bastan dener.
|
||||
@@ -1547,6 +1666,10 @@ class H(http.server.BaseHTTPRequestHandler):
|
||||
if hedef and hedef != gelen:
|
||||
req["model"] = slug = hedef
|
||||
log(f"[{n} {gelen}] sohbet yonlendirmesi: istek {hedef} modeline gidiyor")
|
||||
try:
|
||||
slug = karar_asamasi(n, req, sohbet, slug)
|
||||
except Exception as e:
|
||||
log(f"[{n} {slug}] karar asamasi atlandi: {type(e).__name__}: {e}")
|
||||
mapping = MODEL_MAP.get(slug)
|
||||
if CFG.get("nokta_karsila", True) and nokta_mesaji_mi(req) and model_gecis.tur_ortasi(req, model_gecis.son_sira(req["input"])):
|
||||
# Calisan turun icine dusen nokta secimi tasimaz (olcum 10 Ekim 2026) ve cevaplanirsa isi yarida
|
||||
@@ -1733,14 +1856,19 @@ class H(http.server.BaseHTTPRequestHandler):
|
||||
open(os.path.join(LOGDIR, f"{n:04d}_req.json"), "wb").write(data)
|
||||
hdrs = {"Authorization": f"Bearer {route['key']}", "Content-Type": "application/json", "Accept": "text/event-stream", "User-Agent": UA}
|
||||
last = None
|
||||
# Katman denetimi (jev_denetim.py): Sesli-Hizli-Zeki dışında ek modeller Jev'e sorulur; gecmezse adim
|
||||
# sirayla Katman-1 -> 2 -> 3 -> 4 yapar (secili model zaten katmansa ondan sonrakiler). Katman-4 sorulmadan
|
||||
# gecer. Katmanlar OmniRoute combolaridir: yedek her zaman AITURK gecidi (ide_route) uzerinden acilir,
|
||||
# Katman denetimi (jev_denetim.py): muaf listesi disindaki ek modeller Jev'e sorulur; gecmezse adim
|
||||
# ayardaki zinciri izler (14 Ekim 2026: Katman-1 -> 2 -> 3; secili model zaten katmansa ondan sonrakiler).
|
||||
# Muaf katman sorulmadan gecer. Katmanlar OmniRoute combolaridir: yedek her zaman AITURK gecidi (ide_route) uzerinden acilir,
|
||||
# secili model baska bir rotada olsa bile. Gecit yoksa denetim yapilmaz. Ozet isteginde ve aracsiz
|
||||
# (rewriter yok) yolda kapalidir.
|
||||
jev_rota = ROUTES.get(ide_route)
|
||||
jev_etkin = jev_denetim.etkin_mi() and not (ozet_tetik or rewriter is None or jev_rota is None or not jev_denetim.anahtar())
|
||||
jev_sira = jev_denetim.sonrakiler(target) if jev_etkin else []
|
||||
if jev_sira:
|
||||
try:
|
||||
jev_sira = sigan_katmanlar(jev_sira, req, tag)
|
||||
except Exception as e:
|
||||
log(f"{tag} katman pencere kontrolu atlandi: {type(e).__name__}: {e}")
|
||||
jev_istek = jev_denetim.son_istek(req) if jev_etkin else ""
|
||||
|
||||
def jev_ac(ad):
|
||||
@@ -1753,6 +1881,28 @@ class H(http.server.BaseHTTPRequestHandler):
|
||||
timeout=UPSTREAM_IDLE_TIMEOUT)
|
||||
return (yanit, SseRewriter(custom, hedef=ad, tasma=self._tasma_ogren, aracsiz=self._aracsiz_ogren, tool_names=tool_names),
|
||||
f"[{n} {slug}=>{ad}]")
|
||||
|
||||
def jev_devam(ad, onceki):
|
||||
"""finishCheck: ayni hedefe, erken biten cevap ve not eklenmis istek. Yalniz AITURK gecidindeki hedefler."""
|
||||
if ad == target and route_name != ide_route:
|
||||
raise ValueError("hedef AITURK gecidinde degil")
|
||||
devam_req = json.loads(data)
|
||||
devam_req["model"] = ad
|
||||
devam_req.setdefault("input", []).extend([
|
||||
{"type": "message", "role": "assistant", "content": [{"type": "output_text", "text": onceki}]},
|
||||
{"type": "message", "role": "user", "content": [
|
||||
{"type": "input_text", "text": jev_settings.read()[0]["finishCheck"]["note"]}]}])
|
||||
jev_hdrs = {"Authorization": f"Bearer {jev_rota['key']}", "Content-Type": "application/json",
|
||||
"Accept": "text/event-stream", "User-Agent": UA}
|
||||
yanit = urllib.request.urlopen(urllib.request.Request(
|
||||
jev_rota["url"], data=json.dumps(devam_req, ensure_ascii=False).encode("utf-8"), headers=jev_hdrs, method="POST"),
|
||||
timeout=UPSTREAM_IDLE_TIMEOUT)
|
||||
return (yanit, SseRewriter(custom, hedef=ad, tasma=self._tasma_ogren, aracsiz=self._aracsiz_ogren, tool_names=tool_names),
|
||||
f"[{n} {slug}=>{ad} devam]")
|
||||
try:
|
||||
jev_bitis = jev_devam if jev_etkin and jev_settings.read()[0]["finishCheck"]["enabled"] else None
|
||||
except Exception:
|
||||
jev_bitis = None
|
||||
# A 9Router combo (Fusion-*, no provider prefix) runs a whole panel upstream. Once it may
|
||||
# have started (timeout, 504, cut stream) resending it runs and bills it twice (28 Sep 2026);
|
||||
# only failures before the work starts (no connection, 500/502/503/529) are retried.
|
||||
@@ -1803,7 +1953,7 @@ class H(http.server.BaseHTTPRequestHandler):
|
||||
time.sleep(min(2 * attempt, 8)); continue
|
||||
# Only the resolved exact voice target owns this sideband protocol.
|
||||
# Other models retain their original response/Jev/stream pipeline.
|
||||
if target == 'Sesli-Hizli-Zeki':
|
||||
if jev_denetim.izleme_mi(target):
|
||||
# Capture was taken before rewriting/request dispatch. Only this exact
|
||||
# route credential may authenticate the returned public receipt.
|
||||
correlation = self.watch_correlation
|
||||
@@ -1825,7 +1975,9 @@ class H(http.server.BaseHTTPRequestHandler):
|
||||
log(f"{tag} ozet kopru: {akis.sonuc_notu()}")
|
||||
return sonuc
|
||||
if jev_etkin and resp.status == 200:
|
||||
return self._denetimli_akis(resp, tag, rewriter, izle, jev_istek, jev_sira, jev_ac, hedef=target)
|
||||
# devam yalniz finishCheck acikken gecer; kapaliyken cagri eskisiyle birebir aynidir.
|
||||
return self._denetimli_akis(resp, tag, rewriter, izle, jev_istek, jev_sira, jev_ac, hedef=target,
|
||||
**({"devam": jev_bitis} if jev_bitis else {}))
|
||||
return self._stream(resp, resp.status, tag, rewriter, izle)
|
||||
if jev_etkin: # secili modele ulasilamadi: adimi siradaki katman yapar
|
||||
return self._denetimli_akis(None, tag, rewriter, izle, jev_istek, jev_sira, jev_ac, hedef=target)
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
"""Layer check (11 Oct 2026): Jev passes or blocks each step of Katman-1..3; a blocked step is redone by the next
|
||||
layer. Katman-4 is never asked. No exceptions."""
|
||||
layer. The last layer is never asked. No exceptions.
|
||||
|
||||
14 Oct 2026 defaults: Katman-1 -> Katman-2 -> Katman-3; Katman-3, Chat-Model and bekci-sesli-bekci always pass."""
|
||||
import ast
|
||||
import io
|
||||
import json
|
||||
@@ -60,18 +62,19 @@ class Karar(unittest.TestCase):
|
||||
|
||||
class Katman(unittest.TestCase):
|
||||
def test_a_non_layer_model_falls_through_every_layer_from_the_top(self):
|
||||
self.assertEqual(jev_denetim.sonrakiler("Uncensored-Uygulama"),
|
||||
["Katman-1", "Katman-2", "Katman-3", "Katman-4"])
|
||||
self.assertEqual(jev_denetim.sonrakiler("ide/some-model"),
|
||||
["Katman-1", "Katman-2", "Katman-3", "Katman-4"])
|
||||
self.assertEqual(jev_denetim.sonrakiler("Uncensored-Uygulama"), ["Katman-1", "Katman-2", "Katman-3"])
|
||||
self.assertEqual(jev_denetim.sonrakiler("ide/some-model"), ["Katman-1", "Katman-2", "Katman-3"])
|
||||
|
||||
def test_a_selected_layer_only_tries_the_ones_after_it(self):
|
||||
self.assertEqual(jev_denetim.sonrakiler("Katman-1"), ["Katman-2", "Katman-3", "Katman-4"])
|
||||
self.assertEqual(jev_denetim.sonrakiler("Katman-2"), ["Katman-3", "Katman-4"])
|
||||
self.assertEqual(jev_denetim.sonrakiler("Katman-3"), ["Katman-4"])
|
||||
self.assertEqual(jev_denetim.sonrakiler("Katman-1"), ["Katman-2", "Katman-3"])
|
||||
self.assertEqual(jev_denetim.sonrakiler("Katman-2"), ["Katman-3"])
|
||||
# The always-pass last layer only uses the general fallback on transport failure; no revisits.
|
||||
self.assertEqual(jev_denetim.sonrakiler("Katman-3"), ["Katman-1", "Katman-2"])
|
||||
|
||||
def test_last_layer_is_never_asked(self):
|
||||
self.assertTrue(jev_denetim.muaf_mi("Katman-4"))
|
||||
self.assertTrue(jev_denetim.muaf_mi("Katman-3"))
|
||||
self.assertTrue(jev_denetim.muaf_mi("Chat-Model"))
|
||||
self.assertFalse(jev_denetim.muaf_mi("Katman-4"))
|
||||
self.assertEqual(jev_denetim.sonrakiler(None), [])
|
||||
|
||||
|
||||
@@ -139,7 +142,7 @@ class Akis(unittest.TestCase):
|
||||
self.h.send_response = Mock()
|
||||
self.h.send_header = Mock()
|
||||
self.h.end_headers = Mock()
|
||||
self.cevap = {"Katman-1": sse("bir"), "Katman-2": sse("iki"), "Katman-3": sse("uc"), "Katman-4": sse("dort")}
|
||||
self.cevap = {"Katman-1": sse("bir"), "Katman-2": sse("iki"), "Katman-3": sse("uc")}
|
||||
|
||||
def sor(self, istek, metin, key):
|
||||
self.asked.append(metin)
|
||||
@@ -162,7 +165,7 @@ class Akis(unittest.TestCase):
|
||||
raw = rest[n + 2:]
|
||||
return out
|
||||
|
||||
def calistir(self, body, sira=("Katman-2", "Katman-3", "Katman-4")):
|
||||
def calistir(self, body, sira=("Katman-2", "Katman-3")):
|
||||
self.h._denetimli_akis(Yanit(body), "[1]", None, None, "yap", list(sira), self.ac)
|
||||
|
||||
def test_a_passed_step_is_delivered_unchanged(self):
|
||||
@@ -176,7 +179,7 @@ class Akis(unittest.TestCase):
|
||||
body = sse("Bunu yapamam.")
|
||||
self.puanlar = [{"ret": 0.9, "sart": 0.0}]
|
||||
self.h._denetimli_akis(Yanit(body), "[1]", None, None, "yap",
|
||||
list(jev_denetim.KATMAN), self.ac, hedef="Sesli-Hizli-Zeki")
|
||||
list(jev_denetim.KATMAN), self.ac, hedef="Chat-Model")
|
||||
self.assertEqual(self.sent(), body)
|
||||
self.assertEqual(self.asked, [])
|
||||
self.assertEqual(self.opened, [])
|
||||
@@ -184,14 +187,13 @@ class Akis(unittest.TestCase):
|
||||
def test_voice_transport_failure_still_checks_the_fallback_model(self):
|
||||
self.puanlar = [{"ret": 0.9, "sart": 0.0}]
|
||||
self.h._denetimli_akis(None, "[1]", None, None, "yap",
|
||||
list(jev_denetim.KATMAN), self.ac, hedef="Sesli-Hizli-Zeki")
|
||||
list(jev_denetim.KATMAN), self.ac, hedef="Chat-Model")
|
||||
self.assertEqual(self.opened, ["Katman-1", "Katman-2"])
|
||||
self.assertEqual(self.asked, ["bir", "iki"])
|
||||
self.assertEqual(self.sent(), self.cevap["Katman-2"])
|
||||
|
||||
def test_voice_lookalikes_are_still_checked(self):
|
||||
for target in ("sesli-hizli-zeki", "Sesli-Hizli-Zeki-v2", "ide/Sesli-Hizli-Zeki",
|
||||
"Sesli-Hizli-Zeki ", "Other-Combo"):
|
||||
for target in ("chat-model", "Chat-Model-v2", "ide/Chat-Model", "Chat-Model ", "Other-Combo"):
|
||||
with self.subTest(target=target):
|
||||
self.setUp()
|
||||
self.puanlar = [{"ret": 0.9, "sart": 0.0}]
|
||||
@@ -202,7 +204,7 @@ class Akis(unittest.TestCase):
|
||||
|
||||
def test_a_blocked_non_layer_model_goes_to_layer_1_first(self):
|
||||
self.puanlar = [{"ret": 0.9, "sart": 0.0}] # the selected (non-layer) model is blocked
|
||||
self.calistir(sse("Bunu yapamam."), sira=("Katman-1", "Katman-2", "Katman-3", "Katman-4"))
|
||||
self.calistir(sse("Bunu yapamam."), sira=("Katman-1", "Katman-2", "Katman-3"))
|
||||
self.assertEqual(self.sent(), self.cevap["Katman-1"])
|
||||
self.assertEqual(self.opened, ["Katman-1"])
|
||||
self.assertEqual(self.asked, ["Bunu yapamam.", "bir"])
|
||||
@@ -216,17 +218,17 @@ class Akis(unittest.TestCase):
|
||||
self.assertEqual(self.asked, ["Bunu yapamam.", "iki"])
|
||||
self.h.send_response.assert_called_once()
|
||||
|
||||
def test_every_layer_blocked_ends_at_layer_4_unasked(self):
|
||||
self.puanlar = [{"ret": 0.9, "sart": 0.0}] * 3
|
||||
def test_every_layer_blocked_ends_at_layer_3_unasked(self):
|
||||
self.puanlar = [{"ret": 0.9, "sart": 0.0}] * 2
|
||||
self.calistir(sse("hayir"))
|
||||
self.assertEqual(self.sent(), self.cevap["Katman-4"])
|
||||
self.assertEqual(self.opened, ["Katman-2", "Katman-3", "Katman-4"])
|
||||
self.assertEqual(self.asked, ["hayir", "iki", "uc"]) # layer 4 is never asked
|
||||
self.assertEqual(self.sent(), self.cevap["Katman-3"])
|
||||
self.assertEqual(self.opened, ["Katman-2", "Katman-3"])
|
||||
self.assertEqual(self.asked, ["hayir", "iki"]) # the always-pass last layer is never asked
|
||||
|
||||
def test_blocked_layer_3_goes_straight_to_layer_4(self):
|
||||
def test_blocked_layer_2_goes_straight_to_layer_3(self):
|
||||
self.puanlar = [{"ret": 0.0, "sart": 0.8}]
|
||||
self.calistir(sse("kendi sartimla"), sira=("Katman-4",))
|
||||
self.assertEqual(self.sent(), self.cevap["Katman-4"])
|
||||
self.calistir(sse("kendi sartimla"), sira=("Katman-3",))
|
||||
self.assertEqual(self.sent(), self.cevap["Katman-3"])
|
||||
self.assertEqual(self.asked, ["kendi sartimla"])
|
||||
|
||||
def test_a_layer_that_cannot_be_opened_is_skipped(self):
|
||||
@@ -329,9 +331,9 @@ class Akis(unittest.TestCase):
|
||||
|
||||
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.cevap.update({"Katman-2": None, "Katman-3": None})
|
||||
self.calistir(sse("Bunu yapamam."))
|
||||
self.assertEqual(self.opened, ["Katman-2", "Katman-3", "Katman-4"])
|
||||
self.assertEqual(self.opened, ["Katman-2", "Katman-3"])
|
||||
self.assertNotIn(b"yapamam", self.h.wfile.getvalue())
|
||||
self.assertIn(b'response.failed', self.sent())
|
||||
|
||||
|
||||
@@ -0,0 +1,418 @@
|
||||
"""Decision stages (14 Oct 2026): settings schema 2, turn-start decisions, layer window filter, finish check
|
||||
and Dify rules. Every new stage is off by default; the tests turn them on explicitly."""
|
||||
import ast
|
||||
import copy
|
||||
import hashlib
|
||||
import io
|
||||
import json
|
||||
import tempfile
|
||||
import time
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
import jev_denetim
|
||||
import jev_settings
|
||||
import karar_politikasi
|
||||
import model_gecis
|
||||
import pencere
|
||||
import yarim
|
||||
from test_jev_denetim import Yanit, method, sse, user
|
||||
|
||||
SOURCE = Path(__file__).with_name("kopru.py").read_text(encoding="utf-8")
|
||||
TREE = ast.parse(SOURCE)
|
||||
|
||||
|
||||
def fonksiyon(name):
|
||||
return next(n for n in TREE.body if isinstance(n, ast.FunctionDef) and n.name == name)
|
||||
|
||||
|
||||
class Ev(unittest.TestCase):
|
||||
"""A temporary bridge home with a settings file."""
|
||||
|
||||
def setUp(self):
|
||||
self.tmp = tempfile.TemporaryDirectory()
|
||||
self.addCleanup(self.tmp.cleanup)
|
||||
self.home = Path(self.tmp.name)
|
||||
p = patch.object(jev_settings, 'home', return_value=self.home)
|
||||
p.start()
|
||||
self.addCleanup(p.stop)
|
||||
self.settings = copy.deepcopy(jev_settings.DEFAULTS)
|
||||
|
||||
def save(self, settings=None, **extra):
|
||||
data = {'settings': settings if settings is not None else self.settings}
|
||||
data.update(extra)
|
||||
(self.home / 'jev-settings.json').write_text(json.dumps(data), encoding='utf-8')
|
||||
|
||||
|
||||
class Ayarlar(Ev):
|
||||
def test_a_schema_1_file_gets_every_new_stage_disabled(self):
|
||||
eski = {k: v for k, v in self.settings.items() if k not in jev_settings.SECTIONS}
|
||||
self.save(eski)
|
||||
s, _ = jev_settings.read()
|
||||
for bolum in ('prefilter', 'autoRoute', 'finishCheck', 'verifierCheck'):
|
||||
self.assertFalse(s[bolum]['enabled'], bolum)
|
||||
self.assertEqual(s['policy']['source'], 'local')
|
||||
self.assertEqual(s['watchModels'], jev_settings.DEFAULTS['watchModels'])
|
||||
|
||||
def test_a_partial_section_keeps_written_values_and_fills_the_rest(self):
|
||||
self.settings['prefilter'] = {'enabled': True, 'threshold': 0.8}
|
||||
self.save()
|
||||
p = jev_settings.read()[0]['prefilter']
|
||||
self.assertEqual((p['enabled'], p['threshold']), (True, 0.8))
|
||||
self.assertEqual(p['target'], jev_settings.DEFAULTS['prefilter']['target'])
|
||||
|
||||
def test_bad_values_are_rejected(self):
|
||||
for bolum, alan, deger in (('prefilter', 'threshold', 1.5), ('prefilter', 'timeoutSeconds', 0),
|
||||
('autoRoute', 'model', ''), ('finishCheck', 'note', ''),
|
||||
('policy', 'endpoint', 'http://dify.example'), ('policy', 'source', 'web')):
|
||||
with self.subTest(bolum=bolum, alan=alan):
|
||||
s = copy.deepcopy(self.settings)
|
||||
s[bolum][alan] = deger
|
||||
self.save(s)
|
||||
with self.assertRaises(ValueError):
|
||||
jev_settings.read()
|
||||
|
||||
def test_automatic_model_cannot_target_itself(self):
|
||||
self.settings['autoRoute']['targets']['zor'] = 'Otomatik'
|
||||
self.save()
|
||||
with self.assertRaises(ValueError):
|
||||
jev_settings.read()
|
||||
|
||||
def test_watch_channel_is_an_exact_name_from_settings(self):
|
||||
self.settings['watchModels'] = ['Chat-Model']
|
||||
self.save()
|
||||
self.assertTrue(jev_denetim.izleme_mi('Chat-Model'))
|
||||
for ad in ('chat-model', 'Chat-Model ', 'ide/Chat-Model', 'Sesli-Hizli-Zeki'):
|
||||
self.assertFalse(jev_denetim.izleme_mi(ad), ad)
|
||||
|
||||
|
||||
class Politika(Ev):
|
||||
def test_dify_rules_apply_only_when_the_source_is_dify_and_they_are_valid(self):
|
||||
(self.home / jev_settings.POLICY_FILE).write_text(json.dumps(
|
||||
{'ok': True, 'overrides': {'prefilter': {'enabled': True, 'target': 'Katman-3'}}}), encoding='utf-8')
|
||||
self.save()
|
||||
self.assertFalse(jev_settings.read()[0]['prefilter']['enabled']) # source local
|
||||
self.settings['policy']['source'] = 'dify'
|
||||
self.save()
|
||||
p = jev_settings.read()[0]['prefilter']
|
||||
self.assertEqual((p['enabled'], p['target']), (True, 'Katman-3'))
|
||||
self.assertFalse(jev_settings.read(overlay=False)[0]['prefilter']['enabled'])
|
||||
(self.home / jev_settings.POLICY_FILE).write_text(json.dumps(
|
||||
{'ok': True, 'overrides': {'prefilter': {'threshold': 7}}}), encoding='utf-8')
|
||||
self.assertFalse(jev_settings.read()[0]['prefilter']['enabled']) # invalid rules: local in force
|
||||
|
||||
def test_rules_cannot_change_the_chain_or_unknown_sections(self):
|
||||
s = jev_settings.read(overlay=False)[0]
|
||||
with self.assertRaises(ValueError):
|
||||
karar_politikasi.ayikla({'exemptModels': {'x': 1}}, s)
|
||||
with self.assertRaises(ValueError):
|
||||
karar_politikasi.ayikla({'autoRoute': {'targets': {'zor': 'Otomatik'}}}, s)
|
||||
with self.assertRaises(ValueError):
|
||||
karar_politikasi.ayikla('[1, 2]', s)
|
||||
self.assertEqual(karar_politikasi.ayikla('{"autoRoute": {"enabled": true}}', s), {'autoRoute': {'enabled': True}})
|
||||
|
||||
def test_fetch_reads_the_kurallar_output_and_never_raises(self):
|
||||
s = jev_settings.read(overlay=False)[0]
|
||||
|
||||
def yanit(govde):
|
||||
r = Mock()
|
||||
r.__enter__ = lambda self_: self_
|
||||
r.__exit__ = lambda *a: False
|
||||
r.read = lambda n=-1: json.dumps(govde).encode()
|
||||
return r
|
||||
istekler = []
|
||||
|
||||
def urlopen(req, timeout):
|
||||
istekler.append(req)
|
||||
return yanit({'data': {'outputs': {'kurallar': {'finishCheck': {'enabled': True}}}}})
|
||||
sonuc = karar_politikasi.cek(s, 'app-key', urlopen=urlopen)
|
||||
self.assertEqual(sonuc, {'ok': True, 'overrides': {'finishCheck': {'enabled': True}}})
|
||||
self.assertTrue(istekler[0].full_url.endswith('/workflows/run'))
|
||||
self.assertEqual(istekler[0].get_header('Authorization'), 'Bearer app-key')
|
||||
self.assertFalse(karar_politikasi.cek(s, '', urlopen=urlopen)['ok'])
|
||||
self.assertFalse(karar_politikasi.cek(s, 'k', urlopen=Mock(side_effect=OSError('kapali')))['ok'])
|
||||
bos = karar_politikasi.cek(s, 'k', urlopen=lambda req, timeout: yanit({'data': {'outputs': {}}}))
|
||||
self.assertEqual(bos['ok'], False)
|
||||
|
||||
def test_a_failed_fetch_keeps_the_last_good_rules(self):
|
||||
jev_settings.write_policy({'ok': True, 'overrides': {'finishCheck': {'enabled': True}}})
|
||||
kayit = jev_settings.write_policy({'ok': False, 'error': 'Dify kapali'})
|
||||
self.assertTrue(kayit['ok'])
|
||||
self.assertEqual(kayit['overrides'], {'finishCheck': {'enabled': True}})
|
||||
self.assertEqual(kayit['error'], 'Dify kapali')
|
||||
self.settings['policy']['source'] = 'dify'
|
||||
self.save()
|
||||
self.assertTrue(jev_settings.read()[0]['finishCheck']['enabled'])
|
||||
|
||||
|
||||
class OnKarar(Ev):
|
||||
def gonder(self, cevap):
|
||||
self.govdeler = []
|
||||
|
||||
def f(govde, key):
|
||||
self.govdeler.append(govde)
|
||||
if isinstance(cevap, Exception):
|
||||
raise cevap
|
||||
return {'answers': {k: {'noul': v} for k, v in cevap.items()}}
|
||||
return f
|
||||
|
||||
def test_automatic_routing_picks_research_then_hard_then_simple(self):
|
||||
self.settings['autoRoute']['enabled'] = True
|
||||
self.save()
|
||||
t = self.settings['autoRoute']['targets']
|
||||
for puan, hedef in (({'zor': 0.9, 'arastirma': 0.8}, t['arastirma']), ({'zor': 0.9, 'arastirma': 0.1}, t['zor']),
|
||||
({'zor': 0.1, 'arastirma': 0.1}, t['basit'])):
|
||||
sonuc = jev_denetim.on_karar('istek', True, False, 'k', gonder=self.gonder(puan))
|
||||
self.assertEqual(sonuc['hedef'], hedef)
|
||||
self.assertEqual(set(self.govdeler[0]['questions']), {'zor', 'arastirma'})
|
||||
self.assertNotIn('assistant_reply', self.govdeler[0]['state'])
|
||||
|
||||
def test_no_decision_falls_to_the_strong_target(self):
|
||||
self.settings['autoRoute']['enabled'] = True
|
||||
self.save()
|
||||
sonuc = jev_denetim.on_karar('istek', True, False, 'k', gonder=self.gonder(OSError('yok')))
|
||||
self.assertEqual((sonuc['hedef'], sonuc['ret']), (self.settings['autoRoute']['targets']['zor'], None))
|
||||
|
||||
def test_refusal_question_goes_in_the_same_call(self):
|
||||
self.settings['autoRoute']['enabled'] = True
|
||||
self.settings['prefilter']['enabled'] = True
|
||||
self.save()
|
||||
sonuc = jev_denetim.on_karar('istek', True, True, 'k', gonder=self.gonder({'zor': 0.1, 'arastirma': 0.1, 'ret': 0.9}))
|
||||
self.assertEqual(len(self.govdeler), 1)
|
||||
self.assertEqual(set(self.govdeler[0]['questions']), {'zor', 'arastirma', 'ret'})
|
||||
self.assertTrue(sonuc['ret'])
|
||||
|
||||
def test_a_missing_answer_is_no_decision(self):
|
||||
self.settings['prefilter']['enabled'] = True
|
||||
self.save()
|
||||
sonuc = jev_denetim.on_karar('istek', False, True, 'k', gonder=self.gonder({}))
|
||||
self.assertIsNone(sonuc['ret'])
|
||||
|
||||
|
||||
class TurBasi(Ev):
|
||||
"""kopru.karar_asamasi through its own source."""
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.kararlar = []
|
||||
self.logs = []
|
||||
sahte_denetim = Mock(wraps=jev_denetim)
|
||||
sahte_denetim.anahtar = lambda: 'k'
|
||||
sahte_denetim.on_karar = self.on_karar
|
||||
sahte_denetim.muaf_mi = jev_denetim.muaf_mi
|
||||
sahte_denetim.sonrakiler = jev_denetim.sonrakiler
|
||||
sahte_denetim.son_istek = jev_denetim.son_istek
|
||||
self.cevap = {'hedef': None, 'ret': None, 'puan': None}
|
||||
sahte_denetim.son_istek_sirasi = jev_denetim.son_istek_sirasi
|
||||
self.env = {'IDE_PREFIX': 'ide/', 'ON_KARAR': {}, 'ON_KARAR_SINIR': 3, 'jev_settings': jev_settings,
|
||||
'jev_denetim': sahte_denetim, 'model_gecis': model_gecis, 'hashlib': hashlib, 'time': time,
|
||||
'log': self.logs.append}
|
||||
exec(compile(ast.Module(body=[fonksiyon('karar_asamasi')], type_ignores=[]), 'kopru.py', 'exec'), self.env)
|
||||
self.karar = self.env['karar_asamasi']
|
||||
|
||||
def on_karar(self, istek, oto, ret, key):
|
||||
self.kararlar.append((istek, oto, ret))
|
||||
return dict(self.cevap)
|
||||
|
||||
def istek(self, model, *girdi):
|
||||
return {'model': model, 'prompt_cache_key': 'sohbet-1', 'input': list(girdi)}
|
||||
|
||||
def test_everything_off_changes_nothing_and_asks_nothing(self):
|
||||
self.save()
|
||||
req = self.istek('ide/Otomatik', user('yap'))
|
||||
self.assertEqual(self.karar(1, req, 'sohbet-1', 'ide/Otomatik'), 'ide/Otomatik')
|
||||
self.assertEqual(self.kararlar, [])
|
||||
|
||||
def test_automatic_choice_is_kept_for_the_tool_steps_of_the_same_turn(self):
|
||||
self.settings['autoRoute']['enabled'] = True
|
||||
self.save()
|
||||
self.cevap = {'hedef': 'Arastirma', 'ret': None, 'puan': {'zor': 0.2, 'arastirma': 0.9}}
|
||||
req = self.istek('ide/Otomatik', user('kaynaklari karsilastir'))
|
||||
self.assertEqual(self.karar(1, req, 'sohbet-1', 'ide/Otomatik'), 'ide/Arastirma')
|
||||
self.assertEqual(req['model'], 'ide/Arastirma')
|
||||
arac = {'type': 'function_call_output', 'call_id': 'c1', 'output': 'ok'}
|
||||
req2 = self.istek('ide/Otomatik', user('kaynaklari karsilastir'), {'type': 'function_call', 'call_id': 'c1'}, arac)
|
||||
self.assertEqual(self.karar(2, req2, 'sohbet-1', 'ide/Otomatik'), 'ide/Arastirma')
|
||||
self.assertEqual(len(self.kararlar), 1) # same turn: no second question
|
||||
req3 = self.istek('ide/Otomatik', user('kaynaklari karsilastir'), arac, user('simdi kodu duzelt'))
|
||||
self.cevap = {'hedef': 'Ajan', 'ret': None, 'puan': {'zor': 0.9, 'arastirma': 0.1}}
|
||||
self.assertEqual(self.karar(3, req3, 'sohbet-1', 'ide/Otomatik'), 'ide/Ajan')
|
||||
self.assertEqual(len(self.kararlar), 2) # new user message: new decision
|
||||
|
||||
def test_predicted_refusal_starts_at_the_configured_layer(self):
|
||||
self.settings['prefilter']['enabled'] = True
|
||||
self.save()
|
||||
self.cevap = {'hedef': None, 'ret': True, 'puan': {'ret': 0.9}}
|
||||
req = self.istek('ide/codex-gpt-6-astra', user('sert bir roast yaz'))
|
||||
self.assertEqual(self.karar(1, req, 'sohbet-1', 'ide/codex-gpt-6-astra'), 'ide/Katman-2')
|
||||
self.assertEqual(self.kararlar, [('sert bir roast yaz', False, True)])
|
||||
|
||||
def test_exempt_and_later_layers_are_not_asked(self):
|
||||
self.settings['prefilter']['enabled'] = True
|
||||
self.save()
|
||||
for model in ('ide/Chat-Model', 'ide/Katman-2', 'ide/Katman-3', 'codex/gpt-6-astra'):
|
||||
req = self.istek(model, user('yap'))
|
||||
self.assertEqual(self.karar(1, req, 'sohbet-1', model), model)
|
||||
self.assertEqual(self.kararlar, [])
|
||||
|
||||
def test_no_refusal_or_no_decision_keeps_the_model(self):
|
||||
self.settings['prefilter']['enabled'] = True
|
||||
self.save()
|
||||
for cevap in ({'hedef': None, 'ret': False, 'puan': {'ret': 0.1}}, {'hedef': None, 'ret': None, 'puan': None}):
|
||||
self.env['ON_KARAR'].clear()
|
||||
self.cevap = cevap
|
||||
req = self.istek('ide/Ajan', user('yap'))
|
||||
self.assertEqual(self.karar(1, req, 'sohbet-1', 'ide/Ajan'), 'ide/Ajan')
|
||||
self.assertEqual(req['model'], 'ide/Ajan')
|
||||
|
||||
def test_master_switch_off_disables_every_stage(self):
|
||||
self.settings['enabled'] = False
|
||||
self.settings['prefilter']['enabled'] = True
|
||||
self.settings['autoRoute']['enabled'] = True
|
||||
self.save()
|
||||
req = self.istek('ide/Otomatik', user('yap'))
|
||||
self.assertEqual(self.karar(1, req, 'sohbet-1', 'ide/Otomatik'), 'ide/Otomatik')
|
||||
self.assertEqual(self.kararlar, [])
|
||||
|
||||
def test_turn_memory_is_bounded(self):
|
||||
self.settings['prefilter']['enabled'] = True
|
||||
self.save()
|
||||
self.cevap = {'hedef': None, 'ret': False, 'puan': {'ret': 0.1}}
|
||||
for i in range(6):
|
||||
self.karar(i, {'model': 'ide/Ajan', 'prompt_cache_key': f's{i}', 'input': [user('yap')]}, f's{i}', 'ide/Ajan')
|
||||
self.assertLessEqual(len(self.env['ON_KARAR']), 3)
|
||||
|
||||
|
||||
class KatmanPenceresi(unittest.TestCase):
|
||||
def test_a_layer_too_small_for_the_request_is_skipped(self):
|
||||
logs = []
|
||||
env = {'IDE_PREFIX': 'ide/', 'IDE_BILGI': {'ide/Katman-2': {'pencere': 1000}, 'ide/Katman-3': {'pencere': 10 ** 6}},
|
||||
'pencere': pencere, 'log': logs.append}
|
||||
exec(compile(ast.Module(body=[fonksiyon('sigan_katmanlar')], type_ignores=[]), 'kopru.py', 'exec'), env)
|
||||
req = {'input': [user('x' * 20000)]}
|
||||
self.assertEqual(env['sigan_katmanlar'](['Katman-2', 'Katman-3', 'Bilinmeyen'], req, '[1]'), ['Katman-3', 'Bilinmeyen'])
|
||||
self.assertIn('Katman-2', logs[0])
|
||||
|
||||
|
||||
class BitisDenetimi(Ev):
|
||||
"""_denetimli_akis with finishCheck: an answer that stops early goes back to the same model once."""
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.settings['finishCheck']['enabled'] = True
|
||||
self.save()
|
||||
self.asked, self.logs, self.devamlar = [], [], []
|
||||
self.puanlar = []
|
||||
sahte = Mock()
|
||||
sahte.anahtar = lambda: 'k'
|
||||
sahte.sor = self.sor
|
||||
sahte.karar = jev_denetim.karar
|
||||
sahte.bitmedi_mi = jev_denetim.bitmedi_mi
|
||||
sahte.hazirlik_mi = jev_denetim.hazirlik_mi
|
||||
sahte.muaf_mi = jev_denetim.muaf_mi
|
||||
sahte.hata_gecis = jev_denetim.hata_gecis
|
||||
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')
|
||||
exec(compile(ast.Module(body=[method(a) for a in adlar], type_ignores=[]), 'kopru.py', 'exec'), env)
|
||||
self.h = type('H', (), {a: env[a] for a in adlar})()
|
||||
self.h.wfile = io.BytesIO()
|
||||
self.h.send_response = self.h.send_header = self.h.end_headers = Mock()
|
||||
|
||||
def sor(self, istek, metin, key, bitis=False):
|
||||
self.asked.append((metin, bitis))
|
||||
return self.puanlar.pop(0) if self.puanlar else {'ret': 0.0, 'sart': 0.0}
|
||||
|
||||
def devam(self, ad, onceki):
|
||||
self.devamlar.append((ad, onceki))
|
||||
return Yanit(sse('gercekten bitti, cikti: OK')), None, f'[{ad} devam]'
|
||||
|
||||
def calistir(self, body):
|
||||
self.h._denetimli_akis(Yanit(body), '[1]', None, None, 'yap', ['Katman-1'], Mock(), hedef='Ajan', devam=self.devam)
|
||||
return self.gonderilen()
|
||||
|
||||
def gonderilen(self):
|
||||
raw, out = self.h.wfile.getvalue(), b''
|
||||
while raw:
|
||||
size, _, rest = raw.partition(b'\r\n')
|
||||
n = int(size, 16)
|
||||
if n == 0:
|
||||
break
|
||||
out += rest[:n]
|
||||
raw = rest[n + 2:]
|
||||
return out
|
||||
|
||||
def test_an_early_stop_goes_back_once_and_the_retry_is_delivered(self):
|
||||
self.puanlar = [{'ret': 0.0, 'sart': 0.0, 'bitmedi': 0.9}, {'ret': 0.0, 'sart': 0.0, 'bitmedi': 0.9}]
|
||||
out = self.calistir(sse('Bitti.'))
|
||||
self.assertEqual(self.devamlar, [('Ajan', 'Bitti.')])
|
||||
self.assertIn(b'gercekten bitti', out)
|
||||
self.assertNotIn(b'"Bitti."', out)
|
||||
self.assertEqual([b for _, b in self.asked], [True, False]) # the retry is not asked again
|
||||
|
||||
def test_a_finished_answer_is_delivered_unchanged(self):
|
||||
self.puanlar = [{'ret': 0.0, 'sart': 0.0, 'bitmedi': 0.1}]
|
||||
out = self.calistir(sse('Tamam, test ciktisi: 3 passed.'))
|
||||
self.assertEqual(self.devamlar, [])
|
||||
self.assertIn(b'3 passed', out)
|
||||
|
||||
def test_a_refusal_still_goes_down_the_chain_not_back(self):
|
||||
self.puanlar = [{'ret': 0.9, 'sart': 0.0, 'bitmedi': 0.9}]
|
||||
ac = Mock(return_value=(Yanit(sse('katman')), None, '[K1]'))
|
||||
self.h._denetimli_akis(Yanit(sse('Yapamam.')), '[1]', None, None, 'yap', ['Katman-1'], ac, hedef='Ajan', devam=self.devam)
|
||||
self.assertEqual(self.devamlar, [])
|
||||
ac.assert_called_once_with('Katman-1')
|
||||
|
||||
def test_a_failed_retry_delivers_the_first_answer(self):
|
||||
self.puanlar = [{'ret': 0.0, 'sart': 0.0, 'bitmedi': 0.9}]
|
||||
|
||||
def bozuk(ad, onceki):
|
||||
raise OSError('acilamadi')
|
||||
self.h._denetimli_akis(Yanit(sse('Bitti.')), '[1]', None, None, 'yap', [], Mock(), hedef='Ajan', devam=bozuk)
|
||||
self.assertIn(b'Bitti.', self.gonderilen())
|
||||
|
||||
def test_without_the_stage_the_finish_question_is_not_sent(self):
|
||||
govdeler = []
|
||||
|
||||
def gonder(govde, key):
|
||||
govdeler.append(govde)
|
||||
return {'answers': {'ret': {'noul': 0.1}, 'sart': {'noul': 0.1}}}
|
||||
puan = jev_denetim.sor('i', 'c', 'k', gonder=gonder)
|
||||
self.assertNotIn('bitmedi', govdeler[0]['questions'])
|
||||
self.assertNotIn('bitmedi', puan)
|
||||
puan = jev_denetim.sor('i', 'c', 'k', gonder=gonder, bitis=True)
|
||||
self.assertIn('bitmedi', govdeler[1]['questions'])
|
||||
self.assertEqual(set(puan), {'ret', 'sart'}) # unanswered extra question: main verdict stands
|
||||
self.assertFalse(jev_denetim.bitmedi_mi(puan))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
|
||||
|
||||
class DogrulayiciElemesi(Ev):
|
||||
def test_off_by_default_and_asked_only_when_enabled(self):
|
||||
import io as _io
|
||||
import karar_sor
|
||||
self.save()
|
||||
out = _io.StringIO()
|
||||
self.assertEqual(karar_sor.main(['dogrulayici'], _io.StringIO('{"user_request": "x", "assistant_reply": "kabul"}'), out), 0)
|
||||
self.assertEqual(json.loads(out.getvalue())['enabled'], False)
|
||||
self.settings['verifierCheck']['enabled'] = True
|
||||
self.save()
|
||||
govdeler = []
|
||||
|
||||
def gonder(govde, key):
|
||||
govdeler.append(govde)
|
||||
return {'answers': {'kanitsiz': {'noul': 0.92}}}
|
||||
sonuc = karar_sor.dogrulayici({'user_request': 'olcut', 'assistant_reply': '{"karar":"kabul","bulgular":[]}'}, 'k', gonder)
|
||||
self.assertEqual((sonuc['enabled'], sonuc['kanitsiz'], sonuc['puan']), (True, True, 0.92))
|
||||
self.assertEqual(set(govdeler[0]['questions']), {'kanitsiz'})
|
||||
bozuk = karar_sor.dogrulayici({'user_request': 'o', 'assistant_reply': 'r'}, 'k', Mock(side_effect=OSError('yok')))
|
||||
self.assertIsNone(bozuk['kanitsiz'])
|
||||
|
||||
def test_wrong_usage_exits_2(self):
|
||||
import io as _io
|
||||
import karar_sor
|
||||
self.assertEqual(karar_sor.main(['baska'], _io.StringIO('{}'), _io.StringIO()), 2)
|
||||
Reference in new issue
Block a user