sync: mnogo
This commit is contained in:
419
agentui/providers/adapters/gemini.py
Normal file
419
agentui/providers/adapters/gemini.py
Normal file
@@ -0,0 +1,419 @@
|
||||
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.
|
||||
Reference in New Issue
Block a user