A model picked on the phone only arrives with a typed message that starts a turn. The bridge now handles "modeli X yap" itself: per-chat routing, same-turn handover when a model is working, truthful answer to "which model", and a lone "." joining a running turn no longer cuts the work. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
260 lines
12 KiB
Python
260 lines
12 KiB
Python
"""Sesle ya da yaziyla "modeli sunu yap" komutu ve sohbet basina model yonlendirmesi (10 Ekim 2026).
|
||
|
||
Olcum (sesli sohbet 01a12780): telefonda secilen model bilgisayara yalnizca YENI TUR baslatan yazili mesajla
|
||
ulasiyor. Calisan tura eklenen mesaj (nokta dahil) secimi tasimiyor; dort denemenin ucunde secim kayboldu.
|
||
Codex'in pencereler arasi kanali da sohbet bir pencerede acik degilse istegi kabul etmiyor. Bu yuzden degisiklik
|
||
koprude yapilir: kullanici "modeli gemini flash low yap" der (sesli asistan bunu her soz gibi ana modele iletir),
|
||
kopru cumleyi tanir, o sohbetin isteklerini secilen modele yonlendirir ve cevabi kendisi verir.
|
||
|
||
- Model bostayken: kopru yerel cevap verir ("Sohbetin modeli artik ...").
|
||
- Model calisirken (tur ortasi): komut mesaji kisa bir devir notuna cevrilir ve AYNI tur yeni modelle surer;
|
||
yarim kalan is kesilmez, yeni model kaldigi yerden devam eder.
|
||
- Kullanici sonradan listeden baska model secerse (Codex'in gonderdigi ad degisir) yonlendirme kendiliginden kalkar.
|
||
|
||
Ad bulunamazsa mesaj komut sayilmaz, modele oldugu gibi gider. Birden cok aday varsa degisiklik yapilmaz, adaylar sorulur.
|
||
"""
|
||
import json
|
||
import os
|
||
import re
|
||
import threading
|
||
import time
|
||
|
||
DOSYA = "model-yonlendirme.json"
|
||
OMUR_SN = 7 * 24 * 3600
|
||
EN_COK_KAYIT = 200
|
||
EN_COK_KELIME = 14
|
||
_KILIT = threading.Lock()
|
||
|
||
_TR = str.maketrans("çğıöşüâîû", "cgiosuaiu")
|
||
FIILLER = ("gec", "degis", "yap", "sec", "kullan", "cevir", "olsun", "ayarla")
|
||
SORU = ("hangi", "hangisi", "kim", "kimdir")
|
||
ATLA = {"model", "modeli", "modelini", "modele", "modeline", "modelin", "modelle", "modeller", "su", "bu", "o", "bir",
|
||
"olarak", "ile", "lutfen", "artik", "ana", "sohbet", "sohbetin", "chat", "chatin", "chatte", "simdi", "hadi",
|
||
"bana", "icin", "de", "da", "ve", "mi", "misin", "musun", "mu", "yeni", "diger", "baska", "olan", "codex",
|
||
"ya", "e", "a", "i", "u", "yi", "ye", "na", "ne", "nu", "seni", "sen", "biz", "onu", "ona"}
|
||
# Konusmadan yaziya ceviride sik gorulen yazimlar -> model adindaki parca
|
||
TAKMA = {"cloud": "claude", "klod": "claude", "klaud": "claude", "claud": "claude", "opus": "opus", "opüs": "opus",
|
||
"cemini": "gemini", "jemini": "gemini", "cemin": "gemini", "flas": "flash", "fles": "flash",
|
||
"kuven": "qwen", "kiven": "qwen", "kuen": "qwen", "kven": "qwen", "kodeks": "codex",
|
||
"dusuk": "low", "yuksek": "high", "orta": "medium", "sonet": "sonnet", "haiku": "haiku",
|
||
"sansursuz": "uncensored", "ansansird": "uncensored", "dusunen": "thinking", "dusunmeli": "thinking",
|
||
"ciptii": "gpt", "cipiti": "gpt", "gipiti": "gpt"}
|
||
|
||
|
||
# Listede kisa yazilan saglayici -> konusurken soylenen ad
|
||
SAGLAYICI = {"cc": ("claude",), "agy": ("gemini", "antigravity"), "ag": ("gemini", "antigravity"), "cx": ("codex", "gpt")}
|
||
|
||
|
||
def duz(metin):
|
||
return str(metin or "").lower().translate(_TR)
|
||
|
||
|
||
def parcala(metin):
|
||
"""Harf ve rakam obeklerine ayirir: "agy/gemini-3.8-flash-low" -> agy gemini 3 8 flash low."""
|
||
return [TAKMA.get(p, p) for p in re.findall(r"[a-z]+|\d+", duz(metin))]
|
||
|
||
|
||
def son_sira(girdi):
|
||
"""Son kullanici mesajinin sirasi. Arkasindaki talimat (developer/system) mesajlari atlanir: kancalar
|
||
kullanici mesajinin ardina baglam ekler (olcum 10 Ekim 2026). Baska bir oge varsa -1."""
|
||
if not isinstance(girdi, list):
|
||
return -1
|
||
for i in range(len(girdi) - 1, -1, -1):
|
||
oge = girdi[i]
|
||
if isinstance(oge, dict) and oge.get("type", "message") == "message":
|
||
if oge.get("role") == "user":
|
||
return i
|
||
if oge.get("role") in ("developer", "system"):
|
||
continue
|
||
return -1
|
||
return -1
|
||
|
||
|
||
def son_kullanici_metni(req):
|
||
"""(metin, sira, sesli). Son girdi kullanici metin mesaji degilse (None, -1, False).
|
||
Sesli aktarimda (<realtime_delegation>) yalnizca <input> icindeki soz alinir."""
|
||
girdi = req.get("input")
|
||
sira = son_sira(girdi)
|
||
if sira < 0:
|
||
return None, -1, False
|
||
son = girdi[sira]
|
||
icerik = son.get("content")
|
||
if isinstance(icerik, str):
|
||
metin = icerik
|
||
elif isinstance(icerik, list) and icerik and all(isinstance(p, dict) and p.get("type") in ("input_text", "text")
|
||
for p in icerik):
|
||
metin = "".join(str(p.get("text") or "") for p in icerik)
|
||
else:
|
||
return None, -1, False
|
||
m = re.search(r"<realtime_delegation>.*?<input>(.*?)</input>", metin, re.S)
|
||
if m:
|
||
return m.group(1).strip(), sira, True
|
||
if "<" in metin and ">" in metin:
|
||
return None, -1, False # baska bir sarmalli sistem mesaji; komut aranmaz
|
||
return metin.strip(), sira, False
|
||
|
||
|
||
def komut_coz(metin):
|
||
"""None | {"tur": "gec", "ad": [parcalar]} | {"tur": "sor"}."""
|
||
kelimeler = re.findall(r"[a-z0-9]+", duz(metin))
|
||
if not kelimeler or len(kelimeler) > EN_COK_KELIME or not any(k.startswith("model") for k in kelimeler):
|
||
return None
|
||
if len(kelimeler) <= 8 and any(k in SORU for k in kelimeler):
|
||
return {"tur": "sor"}
|
||
fiil = [k for k in kelimeler if k.startswith(FIILLER) and not k.startswith("model")]
|
||
if fiil:
|
||
ad = [TAKMA.get(k, k) for k in kelimeler if k not in ATLA and k not in fiil]
|
||
ad = [p for k in ad for p in re.findall(r"[a-z]+|\d+", k)]
|
||
return {"tur": "gec", "ad": ad} if ad else None
|
||
return None
|
||
|
||
|
||
def _uyar(sorgu, parca):
|
||
"""Turkce ek almis soz ("geminiye", "opusa") model parcasiyla baslar."""
|
||
if sorgu == parca:
|
||
return True
|
||
return len(parca) >= 3 and sorgu.startswith(parca) and len(sorgu) - len(parca) <= 4 and not sorgu.isdigit()
|
||
|
||
|
||
def eslestir(ad, modeller):
|
||
"""ad: sorgu parcalari; modeller: [(slug, gorunen ad)].
|
||
Doner: {"slug", "ad"} (tek aday) | {"adaylar": [gorunen adlar]} (belirsiz) | None (ad bir modele benzemiyor)."""
|
||
if not ad:
|
||
return None
|
||
puanli = []
|
||
for slug, gorunen in modeller:
|
||
parcalar = parcala(gorunen) or parcala(slug)
|
||
kume = set(parcalar) | set(parcala(slug))
|
||
for kisa, uzunlar in SAGLAYICI.items():
|
||
if kisa in kume:
|
||
kume.update(uzunlar)
|
||
tutan = sum(1 for s in ad if any(_uyar(s, p) for p in kume))
|
||
harf = sum(1 for s in ad if not s.isdigit() and any(_uyar(s, p) for p in kume))
|
||
if harf: # yalniz rakam tutmasi ("3", "5") esleme sayilmaz
|
||
puanli.append((tutan, -len(parcalar), slug, gorunen))
|
||
if not puanli:
|
||
return None
|
||
en = max(p[0] for p in puanli)
|
||
if en < len(ad) and en < max(2, (len(ad) * 2 + 2) // 3):
|
||
return None # sozun cogu hicbir modele uymuyor: komut degil
|
||
iyi = sorted((p for p in puanli if p[0] == en), key=lambda p: (-p[1], p[3]))
|
||
if len(iyi) == 1 or iyi[0][1] != iyi[1][1]:
|
||
return {"slug": iyi[0][2], "ad": iyi[0][3]}
|
||
return {"adaylar": [p[3] for p in iyi[:4]]}
|
||
|
||
|
||
_ARAC = {"function_call", "function_call_output", "custom_tool_call", "custom_tool_call_output", "reasoning",
|
||
"local_shell_call", "local_shell_call_output", "tool_search_call", "tool_search_output"}
|
||
|
||
|
||
def tur_ortasi(req, sira):
|
||
"""Mesaj calisan bir turun icine mi dustu? Geriye dogru, kullanici/talimat mesajlarini atlayarak bakilir:
|
||
once arac ogesi ya da ara (commentary) mesaj gelirse tur suruyordur; once son cevap gelirse tur yeni baslamistir."""
|
||
girdi = req.get("input") or []
|
||
for oge in reversed(girdi[:max(sira, 0)]):
|
||
if not isinstance(oge, dict):
|
||
continue
|
||
tur = oge.get("type", "message")
|
||
if tur in _ARAC:
|
||
return True
|
||
if tur == "message":
|
||
if oge.get("role") == "assistant":
|
||
return oge.get("phase") == "commentary"
|
||
continue
|
||
return False
|
||
|
||
|
||
def metni_degistir(req, sira, yeni):
|
||
"""sira'daki kullanici mesajinin metnini yeni ile degistirir (sesli aktarimda yalnizca <input> icini)."""
|
||
oge = req["input"][sira]
|
||
icerik = oge.get("content")
|
||
eski = icerik if isinstance(icerik, str) else "".join(str(p.get("text") or "") for p in icerik)
|
||
if "<realtime_delegation>" in eski:
|
||
metin = re.sub(r"(<input>).*?(</input>)", lambda m: m.group(1) + yeni + m.group(2), eski, count=1, flags=re.S)
|
||
else:
|
||
metin = yeni
|
||
oge["content"] = metin if isinstance(icerik, str) else [{"type": icerik[0].get("type", "input_text"), "text": metin}]
|
||
|
||
|
||
def gecmisi_temizle(req, modeller):
|
||
"""Gecmisteki (son mesaj disindaki) islenmis "modeli X yap" komutlarini kisa nota cevirir; yoksa sonraki
|
||
model ayni cumleyi yeniden gorup "modeli ben degistiremem" diye cevap veriyor (olcum 11 Ekim 2026).
|
||
Doner: degistirilen mesaj sayisi."""
|
||
girdi = req.get("input")
|
||
if not isinstance(girdi, list):
|
||
return 0
|
||
son = son_sira(girdi)
|
||
n = 0
|
||
for i, oge in enumerate(girdi):
|
||
if i == son or not (isinstance(oge, dict) and oge.get("type", "message") == "message" and oge.get("role") == "user"):
|
||
continue
|
||
metin, _, _ = son_kullanici_metni({"input": [oge]})
|
||
if not metin or "model" not in duz(metin) or metin.startswith("[Köprü notu"):
|
||
continue
|
||
komut = komut_coz(metin)
|
||
sonuc = eslestir(komut["ad"], modeller) if komut and komut["tur"] == "gec" else None
|
||
if sonuc and "slug" in sonuc:
|
||
metni_degistir(req, i, f"[Köprü notu: kullanıcı burada sohbetin modelini {sonuc['ad']} olarak değiştirdi; "
|
||
"değişiklik köprü tarafından yapıldı, senin bir şey yapman gerekmiyor.]")
|
||
n += 1
|
||
return n
|
||
|
||
|
||
def devir_notu(yeni_ad, eski_ad):
|
||
return (f"[Köprü notu: kullanıcı bu sohbetin modelini değiştirdi. Önceki model {eski_ad} idi; şu andan itibaren "
|
||
f"cevap veren model sensin ({yeni_ad}). Yarım kalan işi kaldığı yerden sürdür, baştan başlama. "
|
||
f"Cevabının ilk cümlesinde kısaca \"{yeni_ad} olarak devam ediyorum\" de.]")
|
||
|
||
|
||
class Yonlendirme:
|
||
"""sohbet anahtari -> {"gorulen": Codex'in gonderdigi ad, "hedef": yonlenen ad, "zaman"}; diske yazilir."""
|
||
|
||
def __init__(self, home):
|
||
self.yol = os.path.join(home, DOSYA) if home else None
|
||
self.veri = {}
|
||
if self.yol:
|
||
try:
|
||
with open(self.yol, encoding="utf-8") as f:
|
||
okunan = json.load(f)
|
||
if isinstance(okunan, dict):
|
||
self.veri = {k: v for k, v in okunan.items() if isinstance(v, dict) and v.get("hedef")}
|
||
except (OSError, ValueError):
|
||
pass
|
||
|
||
def _yaz(self):
|
||
if not self.yol:
|
||
return
|
||
try:
|
||
gecici = self.yol + ".tmp"
|
||
with open(gecici, "w", encoding="utf-8") as f:
|
||
json.dump(self.veri, f, ensure_ascii=False)
|
||
os.replace(gecici, self.yol)
|
||
except OSError:
|
||
pass
|
||
|
||
def al(self, sohbet, gelen, simdi=None):
|
||
"""Bu sohbet icin yonlenen ad; yoksa None. Codex baska bir ad gonderiyorsa (kullanici listeden yeni
|
||
model secti) ya da kayit eskidiyse yonlendirme silinir."""
|
||
simdi = simdi or time.time()
|
||
with _KILIT:
|
||
k = self.veri.get(sohbet)
|
||
if not k:
|
||
return None
|
||
if k.get("gorulen") != gelen or simdi - float(k.get("zaman") or 0) > OMUR_SN:
|
||
del self.veri[sohbet]
|
||
self._yaz()
|
||
return None
|
||
return k["hedef"]
|
||
|
||
def koy(self, sohbet, gelen, hedef, simdi=None):
|
||
simdi = simdi or time.time()
|
||
with _KILIT:
|
||
if hedef == gelen:
|
||
self.veri.pop(sohbet, None)
|
||
else:
|
||
self.veri[sohbet] = {"gorulen": gelen, "hedef": hedef, "zaman": simdi}
|
||
if len(self.veri) > EN_COK_KAYIT:
|
||
for eski in sorted(self.veri, key=lambda a: self.veri[a].get("zaman") or 0)[:-EN_COK_KAYIT]:
|
||
del self.veri[eski]
|
||
self._yaz()
|