Files
HadTavern/agentui/providers/adapters/gemini.py
2025-10-03 21:55:24 +03:00

419 lines
19 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.

from __future__ import annotations
import json
from typing import Any, Dict, List, Optional, Tuple
from agentui.providers.adapters.base import ( # [ProviderAdapter](agentui/providers/adapters/base.py:10)
ProviderAdapter,
insert_items,
split_pos_spec,
)
def _is_data_url(u: str) -> bool: # [_is_data_url()](agentui/providers/adapters/gemini.py:14)
return isinstance(u, str) and u.strip().lower().startswith("data:")
def _split_data_url(u: str) -> tuple[str, str]: # [_split_data_url()](agentui/providers/adapters/gemini.py:18)
"""
Возвращает (mime, b64) для data URL.
Поддерживаем форму: data:<mime>;base64,<b64>
"""
try:
header, b64 = u.split(",", 1)
mime = "application/octet-stream"
if header.startswith("data:"):
header2 = header[5:]
if ";base64" in header2:
mime = header2.split(";base64", 1)[0] or mime
elif ";" in header2:
mime = header2.split(";", 1)[0] or mime
elif header2:
mime = header2
return mime, b64
except Exception:
return "application/octet-stream", ""
def _try_json(s: str) -> Any: # [_try_json()](agentui/providers/adapters/gemini.py:38)
try:
obj = json.loads(s)
except Exception:
try:
obj = json.loads(s, strict=False) # type: ignore[call-arg]
except Exception:
return None
for _ in range(2):
if isinstance(obj, str):
st = obj.strip()
if (st.startswith("{") and st.endswith("}")) or (st.startswith("[") and st.endswith("]")):
try:
obj = json.loads(st)
continue
except Exception:
break
break
return obj
class GeminiAdapter(ProviderAdapter): # [GeminiAdapter.__init__()](agentui/providers/adapters/gemini.py:56)
name = "gemini"
# --- Дефолты HTTP ---
def default_base_url(self) -> str:
return "https://generativelanguage.googleapis.com"
def default_endpoint(self, model: str) -> str:
# endpoint с шаблоном model (как в исходном коде)
return "/v1beta/models/{{ model }}:generateContent"
# --- PROMPT: построение провайдерных структур ---
def blocks_struct_for_template(
self,
unified_messages: List[Dict[str, Any]],
context: Dict[str, Any],
node_config: Dict[str, Any],
) -> Dict[str, Any]:
"""
Совместимо с веткой provider in {'gemini','gemini_image'} из
[ProviderCallNode._blocks_struct_for_template()](agentui/pipeline/executor.py:1981).
"""
def _text_from_msg(m: Dict[str, Any]) -> str:
c = m.get("content")
if isinstance(c, list):
texts = [str(p.get("text") or "") for p in c if isinstance(p, dict) and p.get("type") == "text"]
return "\n".join([t for t in texts if t])
return str(c or "")
sys_text = "\n\n".join([_text_from_msg(m) for m in (unified_messages or []) if m.get("role") == "system"]).strip()
contents: List[Dict[str, Any]] = []
for m in (unified_messages or []):
if m.get("role") == "system":
continue
role = "model" if m.get("role") == "assistant" else "user"
c = m.get("content")
parts: List[Dict[str, Any]] = []
if isinstance(c, list):
for p in c:
if not isinstance(p, dict):
continue
if p.get("type") == "text":
parts.append({"text": str(p.get("text") or "")})
elif p.get("type") in {"image_url", "image"}:
url = str(p.get("url") or "")
if _is_data_url(url):
mime, b64 = _split_data_url(url)
parts.append({"inline_data": {"mime_type": mime, "data": b64}})
else:
parts.append({"text": url})
else:
parts.append({"text": str(c or "")})
contents.append({"role": role, "parts": parts})
d: Dict[str, Any] = {
"contents": contents,
"system_text": sys_text,
}
if sys_text:
d["systemInstruction"] = {"parts": [{"text": sys_text}]}
return d
def normalize_segment(self, x: Any) -> List[Dict[str, Any]]:
"""
Совместимо с [_as_gemini_contents()](agentui/pipeline/executor.py:2521).
"""
cnts: List[Dict[str, Any]] = []
try:
if isinstance(x, dict):
if isinstance(x.get("contents"), list):
return list(x.get("contents") or [])
if isinstance(x.get("messages"), list):
# OpenAI → Gemini
for m in (x.get("messages") or []):
if not isinstance(m, dict):
continue
role_raw = str(m.get("role") or "user")
role = "model" if role_raw == "assistant" else "user"
cont = m.get("content")
parts: List[Dict[str, Any]] = []
if isinstance(cont, str):
parts = [{"text": cont}]
elif isinstance(cont, list):
for p in cont:
if not isinstance(p, dict):
continue
if p.get("type") == "text":
parts.append({"text": str(p.get("text") or "")})
elif p.get("type") in {"image_url", "image"}:
# Gemini не принимает внешние URL картинок как image — оставим как текстовую ссылку
url = ""
if isinstance(p.get("image_url"), dict):
url = str((p.get("image_url") or {}).get("url") or "")
elif "url" in p:
url = str(p.get("url") or "")
if url:
parts.append({"text": url})
else:
parts = [{"text": json.dumps(cont, ensure_ascii=False)}]
cnts.append({"role": role, "parts": parts})
return cnts
if isinstance(x, list):
# Gemini contents list already
if all(isinstance(c, dict) and "parts" in c for c in x):
return list(x)
# OpenAI messages list -> Gemini
if all(isinstance(m, dict) and "content" in m for m in x):
out: List[Dict[str, Any]] = []
for m in x:
role_raw = str(m.get("role") or "user")
role = "model" if role_raw == "assistant" else "user"
cont = m.get("content")
parts: List[Dict[str, Any]] = []
if isinstance(cont, str):
parts = [{"text": cont}]
elif isinstance(cont, list):
for p in cont:
if not isinstance(p, dict):
continue
if p.get("type") == "text":
parts.append({"text": str(p.get("text") or "")})
elif p.get("type") in {"image_url", "image"}:
url = ""
if isinstance(p.get("image_url"), dict):
url = str((p.get("image_url") or {}).get("url") or "")
elif "url" in p:
url = str(p.get("url") or "")
if url:
parts.append({"text": url})
else:
parts = [{"text": json.dumps(cont, ensure_ascii=False)}]
out.append({"role": role, "parts": parts})
return out
# Fallback
return [{"role": "user", "parts": [{"text": json.dumps(x, ensure_ascii=False)}]}]
if isinstance(x, str):
try_obj = _try_json(x)
if try_obj is not None:
return self.normalize_segment(try_obj)
return [{"role": "user", "parts": [{"text": x}]}]
return [{"role": "user", "parts": [{"text": json.dumps(x, ensure_ascii=False)}]}]
except Exception:
return [{"role": "user", "parts": [{"text": str(x)}]}]
def filter_items(self, arr: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
"""
Совместимо с [_filter_gemini()](agentui/pipeline/executor.py:2782).
Сохраняем inline_data/inlineData как есть; текстовые части — только непустые.
"""
out: List[Dict[str, Any]] = []
for it in (arr or []):
if not isinstance(it, dict):
continue
parts = it.get("parts") or []
norm_parts = []
for p in parts:
if isinstance(p, dict):
t = p.get("text")
if isinstance(t, str) and t.strip():
norm_parts.append({"text": t})
elif "inline_data" in p or "inlineData" in p:
norm_parts.append(p) # изображения пропускаем как есть
if norm_parts:
out.append({"role": it.get("role", "user"), "parts": norm_parts})
return out
def extract_system_text_from_obj(self, x: Any, render_ctx: Dict[str, Any]) -> Optional[str]:
"""
Совместимо с [_extract_sys_text_from_obj()](agentui/pipeline/executor.py:2676) для Gemini.
"""
try:
# Dict
if isinstance(x, dict):
if "systemInstruction" in x:
si = x.get("systemInstruction")
def _parts_to_text(siobj: Any) -> str:
try:
parts = siobj.get("parts") or []
texts = [
str(p.get("text") or "")
for p in parts
if isinstance(p, dict) and isinstance(p.get("text"), str) and p.get("text").strip()
]
return "\n".join([t for t in texts if t]).strip()
except Exception:
return ""
if isinstance(si, dict):
t = _parts_to_text(si)
if t:
return t
if isinstance(si, list):
texts = []
for p in si:
if isinstance(p, dict) and isinstance(p.get("text"), str) and p.get("text").strip():
texts.append(p.get("text").strip())
t = "\n".join(texts).strip()
if t:
return t
if isinstance(si, str) and si.strip():
return si.strip()
# OpenAI system внутри messages
if isinstance(x.get("messages"), list):
sys_msgs = []
for m in (x.get("messages") or []):
try:
if (str(m.get("role") or "").lower().strip() == "system"):
cont = m.get("content")
if isinstance(cont, str) and cont.strip():
sys_msgs.append(cont.strip())
elif isinstance(cont, list):
for p in cont:
if (
isinstance(p, dict)
and p.get("type") == "text"
and isinstance(p.get("text"), str)
and p.get("text").strip()
):
sys_msgs.append(p.get("text").strip())
except Exception:
continue
if sys_msgs:
return "\n\n".join(sys_msgs).strip()
# List
if isinstance(x, list):
if all(isinstance(m, dict) and "role" in m for m in x):
sys_msgs = []
for m in x:
try:
if (str(m.get("role") or "").lower().strip() == "system"):
cont = m.get("content")
if isinstance(cont, str) and cont.strip():
sys_msgs.append(cont.strip())
elif isinstance(cont, list):
for p in cont:
if (
isinstance(p, dict)
and p.get("type") == "text"
and isinstance(p.get("text"), str)
and p.get("text").strip()
):
sys_msgs.append(p.get("text").strip())
except Exception:
continue
if sys_msgs:
return "\n\n".join(sys_msgs).strip()
# Gemini contents list -> попробуем взять из входящего snapshot
if all(isinstance(c, dict) and "parts" in c for c in x):
try:
inc = (render_ctx.get("incoming") or {}).get("json") or {}
si = inc.get("systemInstruction")
if si is not None:
return self.extract_system_text_from_obj({"systemInstruction": si}, render_ctx)
except Exception:
pass
return None
except Exception:
return None
def combine_segments(
self,
blocks_struct: Dict[str, Any],
pre_segments_raw: List[Dict[str, Any]],
raw_segs: List[str],
render_ctx: Dict[str, Any],
pre_var_paths: set[str],
render_template_simple_fn,
var_macro_fullmatch_re,
detect_vendor_fn,
) -> Dict[str, Any]:
"""
Повторяет ветку provider in {'gemini','gemini_image'} из prompt_combine
([ProviderCallNode.run()](agentui/pipeline/executor.py:2874)).
"""
built: List[Dict[str, Any]] = []
sys_texts: List[str] = []
# 1) Пред‑сегменты
for _pre in (pre_segments_raw or []):
try:
_obj = _pre.get("obj")
items = self.normalize_segment(_obj)
items = self.filter_items(items)
built = insert_items(built, items, _pre.get("pos"))
try:
sx = self.extract_system_text_from_obj(_obj, render_ctx)
if isinstance(sx, str) and sx.strip():
sys_texts.append(sx.strip())
except Exception:
pass
except Exception:
pass
# 2) Основные сегменты
for raw_seg in (raw_segs or []):
body_seg, pos_spec = split_pos_spec(raw_seg)
if body_seg == "[[PROMPT]]":
items = self.filter_items(list(blocks_struct.get("contents", []) or []))
built = insert_items(built, items, pos_spec)
continue
m_pre = var_macro_fullmatch_re.fullmatch(body_seg)
if m_pre:
_p = (m_pre.group(1) or "").strip()
try:
if _p in pre_var_paths:
continue
except Exception:
pass
resolved = render_template_simple_fn(body_seg, render_ctx, render_ctx.get("OUT") or {})
obj = _try_json(resolved)
# debug provider guess
try:
pg = detect_vendor_fn(obj if isinstance(obj, dict) else {})
print(f"DEBUG: prompt_combine seg provider_guess={pg} -> target=gemini pos={pos_spec}")
except Exception:
pass
items = self.normalize_segment(obj if obj is not None else resolved)
items = self.filter_items(items)
built = insert_items(built, items, pos_spec)
try:
sx = self.extract_system_text_from_obj(obj, render_ctx) if obj is not None else None
if isinstance(sx, str) and sx.strip():
sys_texts.append(sx.strip())
except Exception:
pass
if not built:
built = self.filter_items(list(blocks_struct.get("contents", []) or []))
# Merge systemInstruction: PROMPT blocks + gathered sys_texts
existing_si = blocks_struct.get("systemInstruction")
parts = []
if isinstance(existing_si, dict) and isinstance(existing_si.get("parts"), list):
parts = list(existing_si.get("parts") or [])
for s in sys_texts:
parts.append({"text": s})
new_si = {"parts": parts} if parts else existing_si
return {"contents": built, "systemInstruction": new_si, "system_text": blocks_struct.get("system_text")}
def prompt_fragment(self, pm_struct: Dict[str, Any], node_config: Dict[str, Any]) -> str:
"""
Совместимо с веткой provider in {'gemini','gemini_image'} в построении [[PROMPT]]
([ProviderCallNode.run()](agentui/pipeline/executor.py:3103)).
"""
parts = []
contents = pm_struct.get("contents")
if contents is not None:
parts.append('"contents": ' + json.dumps(contents, ensure_ascii=False))
sysi = pm_struct.get("systemInstruction")
if sysi is not None:
parts.append('"systemInstruction": ' + json.dumps(sysi, ensure_ascii=False))
return ", ".join(parts)
class GeminiImageAdapter(GeminiAdapter): # [GeminiImageAdapter.__init__()](agentui/providers/adapters/gemini.py:332)
name = "gemini_image"
# Вся логика такая же, как у Gemini (generateContent), включая defaults.