sync: mnogo

This commit is contained in:
2025-10-03 21:55:24 +03:00
parent 2abfbb4b1a
commit 86182c0808
22 changed files with 4462 additions and 1469 deletions

View File

@@ -0,0 +1,34 @@
from __future__ import annotations
"""
Пакет адаптеров провайдеров для ProviderCall.
Экспортируем:
- ProviderAdapter базовый класс
- Реализации: OpenAIAdapter, GeminiAdapter, GeminiImageAdapter, ClaudeAdapter
- Утилиты: default_base_url_for, insert_items, split_pos_spec
"""
from .base import ( # [ProviderAdapter](agentui/providers/adapters/base.py:10)
ProviderAdapter,
default_base_url_for,
insert_items,
split_pos_spec,
)
from .openai import OpenAIAdapter # [OpenAIAdapter](agentui/providers/adapters/openai.py:39)
from .gemini import ( # [GeminiAdapter](agentui/providers/adapters/gemini.py:56)
GeminiAdapter,
GeminiImageAdapter, # [GeminiImageAdapter](agentui/providers/adapters/gemini.py:332)
)
from .claude import ClaudeAdapter # [ClaudeAdapter](agentui/providers/adapters/claude.py:56)
__all__ = [
"ProviderAdapter",
"OpenAIAdapter",
"GeminiAdapter",
"GeminiImageAdapter",
"ClaudeAdapter",
"default_base_url_for",
"insert_items",
"split_pos_spec",
]

View File

@@ -0,0 +1,148 @@
from __future__ import annotations
from abc import ABC, abstractmethod
from typing import Any, Dict, List, Optional, Tuple
class ProviderAdapter(ABC): # [ProviderAdapter.__init__()](agentui/providers/adapters/base.py:10)
"""
Базовый интерфейс адаптера провайдера для ProviderCall.
Задачи адаптера:
- blocks_struct_for_template: собрать pm_struct из унифицированных сообщений (Prompt Blocks)
- normalize_segment/filter_items: привести произвольный сегмент к целевой провайдерной структуре и отфильтровать пустое
- extract_system_text_from_obj: вытащить системный текст из произвольного сегмента (если он там есть)
- combine_segments: слить pre_segments (prompt_preprocess) и prompt_combine с blocks_struct → итоговый pm_struct
- prompt_fragment: собрать строку JSON-фрагмента для подстановки в [[PROMPT]]
- default_endpoint/default_base_url: дефолты путей и базовых URL
"""
name: str = "base"
# --- Дефолты HTTP ---
@abstractmethod
def default_base_url(self) -> str:
...
@abstractmethod
def default_endpoint(self, model: str) -> str:
...
# --- PROMPT: построение провайдерных структур ---
@abstractmethod
def blocks_struct_for_template(
self,
unified_messages: List[Dict[str, Any]],
context: Dict[str, Any],
node_config: Dict[str, Any],
) -> Dict[str, Any]:
"""
Из унифицированных сообщений [{role, content}] (включая text+image) собрать pm_struct
для целевого провайдера. Результат должен быть совместим с текущей логикой [[PROMPT]].
"""
...
@abstractmethod
def normalize_segment(self, obj: Any) -> List[Dict[str, Any]]:
"""
Привести произвольный сегмент (dict/list/str/числа) к целевому массиву элементов
(например, messages для openai/claude или contents для gemini).
"""
...
@abstractmethod
def filter_items(self, items: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
"""
Отфильтровать пустые элементы (пустые тексты и т.п.) согласно правилам провайдера.
"""
...
@abstractmethod
def extract_system_text_from_obj(self, obj: Any, render_ctx: Dict[str, Any]) -> Optional[str]:
"""
Вытащить системный текст из произвольного объекта фрагмента:
- OpenAI: messages[*] role=system
- Gemini: systemInstruction.parts[].text
- Claude: top-level system (string/blocks)
Возвращает строку или None.
"""
...
@abstractmethod
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, # (s, ctx, out_map) -> str
var_macro_fullmatch_re, # _VAR_MACRO_RE.fullmatch
detect_vendor_fn, # detect_vendor
) -> Dict[str, Any]:
"""
Слить blocks_struct c массивами pre_segments_raw и строковыми raw_segs (prompt_combine)
и вернуть итоговый pm_struct. Поведение должно повторять текущее (позиционирование, фильтр пустых,
сбор системного текста).
"""
...
@abstractmethod
def prompt_fragment(self, pm_struct: Dict[str, Any], node_config: Dict[str, Any]) -> str:
"""
Сформировать строку JSON-фрагмента для [[PROMPT]] по итоговому pm_struct.
"""
...
# --- Общие утилиты для позиционирования и парсинга директив ---------------------
def insert_items(base: List[Any], items: List[Any], pos_spec: Optional[str]) -> List[Any]: # [insert_items()](agentui/providers/adapters/base.py:114)
if not items:
return base
if not pos_spec or str(pos_spec).lower() == "append":
base.extend(items)
return base
p = str(pos_spec).lower()
if p == "prepend":
return list(items) + base
try:
idx = int(pos_spec) # type: ignore[arg-type]
if idx < 0:
idx = len(base) + idx
if idx < 0:
idx = 0
if idx > len(base):
idx = len(base)
return base[:idx] + list(items) + base[idx:]
except Exception:
base.extend(items)
return base
def split_pos_spec(s: str) -> Tuple[str, Optional[str]]: # [split_pos_spec()](agentui/providers/adapters/base.py:135)
"""
Отделить директиву @pos=... от тела сегмента.
Возвращает (body, pos_spec | None).
"""
import re as _re
m = _re.search(r"@pos\s*=\s*(prepend|append|-?\d+)\s*$", str(s or ""), flags=_re.IGNORECASE)
if not m:
return (str(s or "").strip(), None)
body = str(s[: m.start()]).strip()
return (body, str(m.group(1)).strip().lower())
# --- Дефолтные base_url по "вендору" (используется RawForward) ------------------
def default_base_url_for(vendor: str) -> Optional[str]: # [default_base_url_for()](agentui/providers/adapters/base.py:149)
v = (vendor or "").strip().lower()
if v == "openai":
return "https://api.openai.com"
if v == "claude" or v == "anthropic":
return "https://api.anthropic.com"
if v == "gemini" or v == "gemini_image":
return "https://generativelanguage.googleapis.com"
return None

View File

@@ -0,0 +1,475 @@
from __future__ import annotations
import json
from typing import Any, Dict, List, Optional
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/claude.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/claude.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/claude.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 ClaudeAdapter(ProviderAdapter): # [ClaudeAdapter.__init__()](agentui/providers/adapters/claude.py:56)
name = "claude"
# --- Дефолты HTTP ---
def default_base_url(self) -> str:
return "https://api.anthropic.com"
def default_endpoint(self, model: str) -> str:
return "/v1/messages"
# --- 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=='claude' из
[ProviderCallNode._blocks_struct_for_template()](agentui/pipeline/executor.py:2022).
"""
# Системные сообщения как текст
sys_msgs = []
for m in (unified_messages or []):
if m.get("role") == "system":
c = m.get("content")
if isinstance(c, list):
sys_msgs.append("\n".join([str(p.get("text") or "") for p in c if isinstance(p, dict) and p.get("type") == "text"]))
else:
sys_msgs.append(str(c or ""))
sys_text = "\n\n".join([s for s in sys_msgs if s]).strip()
out_msgs = []
for m in (unified_messages or []):
if m.get("role") == "system":
continue
role = m.get("role")
role = role if role in {"user", "assistant"} else "user"
c = m.get("content")
blocks: List[Dict[str, Any]] = []
if isinstance(c, list):
for p in c:
if not isinstance(p, dict):
continue
if p.get("type") == "text":
blocks.append({"type": "text", "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)
blocks.append({"type": "image", "source": {"type": "base64", "media_type": mime, "data": b64}})
else:
blocks.append({"type": "image", "source": {"type": "url", "url": url}})
else:
blocks.append({"type": "text", "text": str(c or "")})
out_msgs.append({"role": role, "content": blocks})
claude_no_system = False
try:
claude_no_system = bool((node_config or {}).get("claude_no_system", False))
except Exception:
claude_no_system = False
if claude_no_system:
if sys_text:
out_msgs = [{"role": "user", "content": [{"type": "text", "text": sys_text}]}] + out_msgs
return {
"messages": out_msgs,
"system_text": sys_text,
}
d = {
"system_text": sys_text,
"messages": out_msgs,
}
if sys_text:
# Prefer system as a plain string (proxy compatibility)
d["system"] = sys_text
return d
def normalize_segment(self, x: Any) -> List[Dict[str, Any]]:
"""
Совместимо с [_as_claude_messages()](agentui/pipeline/executor.py:2602).
"""
msgs: List[Dict[str, Any]] = []
try:
if isinstance(x, dict):
# Dict with messages (OpenAI-like)
if isinstance(x.get("messages"), list):
x = x.get("messages") or []
# fallthrough to list mapping below
elif isinstance(x.get("contents"), list):
# Gemini -> Claude
for c in (x.get("contents") or []):
if not isinstance(c, dict):
continue
role_raw = str(c.get("role") or "user")
role = "assistant" if role_raw == "model" else ("user" if role_raw not in {"user", "assistant"} else role_raw)
parts = c.get("parts") or []
text = "\n".join([str(p.get("text")) for p in parts if isinstance(p, dict) and isinstance(p.get("text"), str)]).strip()
msgs.append({"role": role, "content": [{"type": "text", "text": text}]})
return msgs
if isinstance(x, list):
# Gemini contents list -> Claude messages
if all(isinstance(c, dict) and "parts" in c for c in x):
for c in x:
role_raw = str(c.get("role") or "user")
role = "assistant" if role_raw == "model" else ("user" if role_raw not in {"user", "assistant"} else role_raw)
blocks: List[Dict[str, Any]] = []
for p in (c.get("parts") or []):
if isinstance(p, dict) and isinstance(p.get("text"), str):
txt = p.get("text").strip()
if txt:
blocks.append({"type": "text", "text": txt})
msgs.append({"role": role, "content": blocks or [{"type": "text", "text": ""}]})
return msgs
# OpenAI messages list -> Claude
if all(isinstance(m, dict) and "content" in m for m in x):
out: List[Dict[str, Any]] = []
for m in x:
role = m.get("role", "user")
cont = m.get("content")
blocks: List[Dict[str, Any]] = []
if isinstance(cont, str):
blocks.append({"type": "text", "text": cont})
elif isinstance(cont, list):
for p in cont:
if not isinstance(p, dict):
continue
if p.get("type") == "text":
blocks.append({"type": "text", "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:
blocks.append({"type": "image", "source": {"type": "url", "url": url}})
else:
blocks.append({"type": "text", "text": json.dumps(cont, ensure_ascii=False)})
out.append({"role": role if role in {"user", "assistant"} else "user", "content": blocks})
return out
# Fallback
return [{"role": "user", "content": [{"type": "text", "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", "content": [{"type": "text", "text": x}]}]
return [{"role": "user", "content": [{"type": "text", "text": json.dumps(x, ensure_ascii=False)}]}]
except Exception:
return [{"role": "user", "content": [{"type": "text", "text": str(x)}]}]
def filter_items(self, arr: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
"""
Совместимо с [_filter_claude()](agentui/pipeline/executor.py:2820).
"""
out: List[Dict[str, Any]] = []
for m in (arr or []):
if not isinstance(m, dict):
continue
blocks = m.get("content")
if isinstance(blocks, list):
norm = []
for b in blocks:
if isinstance(b, dict) and b.get("type") == "text":
txt = str(b.get("text") or "")
if txt.strip():
norm.append({"type": "text", "text": txt})
if norm:
out.append({"role": m.get("role", "user"), "content": norm})
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).
"""
try:
# Dict objects
if isinstance(x, dict):
# Gemini systemInstruction
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()
# Claude system (string or blocks)
if "system" in x and not ("messages" in x and isinstance(x.get("messages"), list)):
sysv = x.get("system")
if isinstance(sysv, str) and sysv.strip():
return sysv.strip()
if isinstance(sysv, list):
texts = [
str(b.get("text") or "")
for b in sysv
if isinstance(b, dict)
and (b.get("type") == "text")
and isinstance(b.get("text"), str)
and b.get("text").strip()
]
t = "\n".join([t for t in texts if t]).strip()
if t:
return t
# OpenAI messages with role=system
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 objects
if isinstance(x, list):
# OpenAI messages list with role=system
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: попробуем прочитать systemInstruction из входящего 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=='claude' из prompt_combine
([ProviderCallNode.run()](agentui/pipeline/executor.py:2998)).
"""
built3: List[Dict[str, Any]] = []
sys_texts: List[str] = []
# Нода-конфиг (для claude_no_system) передан через render_ctx['_node_config'], см. интеграцию
node_cfg = {}
try:
nc = render_ctx.get("_node_config")
if isinstance(nc, dict):
node_cfg = nc
except Exception:
node_cfg = {}
claude_no_system = False
try:
claude_no_system = bool(node_cfg.get("claude_no_system", False))
except Exception:
claude_no_system = False
# Пред‑сегменты
for _pre in (pre_segments_raw or []):
try:
_obj = _pre.get("obj")
items = self.normalize_segment(_obj)
items = self.filter_items(items)
built3 = insert_items(built3, 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
# Основные сегменты
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("messages", []) or []))
built3 = insert_items(built3, 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:
# Skip duplicate var segment - already inserted via prompt_preprocess (filtered)
continue
except Exception:
pass
resolved = render_template_simple_fn(body_seg, render_ctx, render_ctx.get("OUT") or {})
obj = _try_json(resolved)
try:
pg = detect_vendor_fn(obj if isinstance(obj, dict) else {})
print(f"DEBUG: prompt_combine seg provider_guess={pg} -> target=claude pos={pos_spec}")
except Exception:
pass
items = self.normalize_segment(obj if obj is not None else resolved)
items = self.filter_items(items)
built3 = insert_items(built3, 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 built3:
built3 = self.filter_items(list(blocks_struct.get("messages", []) or []))
# Merge system blocks from PROMPT blocks + gathered sys_texts
existing_sys = blocks_struct.get("system") or []
sys_blocks: List[Dict[str, Any]] = []
if isinstance(existing_sys, list):
sys_blocks.extend(existing_sys)
st0 = blocks_struct.get("system_text") or ""
# Ensure PROMPT system_text from blocks is included as a Claude system block
if isinstance(st0, str) and st0.strip():
sys_blocks.append({"type": "text", "text": st0})
for s in sys_texts:
sys_blocks.append({"type": "text", "text": s})
st = "\n\n".join([t for t in [st0] + sys_texts if isinstance(t, str) and t.strip()])
if claude_no_system:
# Prepend system text as a user message instead of top-level system
if st:
built3 = [{"role": "user", "content": [{"type": "text", "text": st}]}] + built3
return {"messages": built3, "system_text": st}
pm_struct = {"messages": built3, "system_text": st}
# Prefer array of system blocks when possible; fallback to single text block
if sys_blocks:
pm_struct["system"] = sys_blocks
elif st:
pm_struct["system"] = [{"type": "text", "text": st}]
return pm_struct
def prompt_fragment(self, pm_struct: Dict[str, Any], node_config: Dict[str, Any]) -> str:
"""
Совместимо с веткой provider=='claude' в построении [[PROMPT]]
([ProviderCallNode.run()](agentui/pipeline/executor.py:3125)).
"""
parts: List[str] = []
# Учитываем флаг совместимости: при claude_no_system не добавляем top-level "system"
claude_no_system = False
try:
claude_no_system = bool((node_config or {}).get("claude_no_system", False))
except Exception:
claude_no_system = False
if not claude_no_system:
# Предпочитаем массив блоков system, если он есть; иначе строковый system_text
sys_val = pm_struct.get("system", None)
if sys_val is None:
sys_val = pm_struct.get("system_text")
if sys_val:
parts.append('"system": ' + json.dumps(sys_val, ensure_ascii=False))
msgs = pm_struct.get("messages")
if msgs is not None:
parts.append('"messages": ' + json.dumps(msgs, ensure_ascii=False))
return ", ".join(parts)

View 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.

View File

@@ -0,0 +1,398 @@
from __future__ import annotations
import json
import re
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 _try_json(s: str) -> Any: # [_try_json()](agentui/providers/adapters/openai.py:16)
"""
Парсит JSON из строки. Пермиссивный режим и двукратная распаковка строк, как в старой логике.
Возвращает dict/list/примитив или None при неудаче.
"""
try:
obj = json.loads(s)
except Exception:
try:
obj = json.loads(s, strict=False) # type: ignore[call-arg]
except Exception:
return None
# Если это строка, которая сама похожа на JSON — пробуем распаковать до 2 раз
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 OpenAIAdapter(ProviderAdapter): # [OpenAIAdapter.__init__()](agentui/providers/adapters/openai.py:39)
name = "openai"
# --- Дефолты HTTP ---
def default_base_url(self) -> str:
return "https://api.openai.com"
def default_endpoint(self, model: str) -> str:
return "/v1/chat/completions"
# --- 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=='openai' из [ProviderCallNode._blocks_struct_for_template()](agentui/pipeline/executor.py:1958).
"""
def _map(m: Dict[str, Any]) -> Dict[str, Any]:
c = m.get("content")
if isinstance(c, list):
parts = []
for p in c:
if isinstance(p, dict) and p.get("type") == "text":
parts.append({"type": "text", "text": str(p.get("text") or "")})
elif isinstance(p, dict) and p.get("type") in {"image_url", "image"}:
url = str(p.get("url") or "")
parts.append({"type": "image_url", "image_url": {"url": url}})
return {"role": m.get("role", "user"), "content": parts}
return {"role": m.get("role", "user"), "content": str(c or "")}
# system_text — склейка всех system-блоков (только текст, без картинок)
sys_text = "\n\n".join(
[
str(m.get("content") or "")
if not isinstance(m.get("content"), list)
else "\n".join(
[str(p.get("text") or "") for p in m.get("content") if isinstance(p, dict) and p.get("type") == "text"]
)
for m in (unified_messages or [])
if m.get("role") == "system"
]
).strip()
return {
"messages": [_map(m) for m in (unified_messages or [])],
"system_text": sys_text,
}
def normalize_segment(self, x: Any) -> List[Dict[str, Any]]:
"""
Совместимо с [_as_openai_messages()](agentui/pipeline/executor.py:2451).
- Поддерживает dict with messages (openai)
- Поддерживает dict/list в стиле Gemini.contents (склейка текстов частей)
- Поддерживает list openai-like messages (нормализация parts)
- Строки/прочее упаковываются как один user message
"""
msgs: List[Dict[str, Any]] = []
try:
# Dict inputs
if isinstance(x, dict):
if isinstance(x.get("messages"), list):
return list(x.get("messages") or [])
if isinstance(x.get("contents"), list):
# Gemini -> OpenAI (text-only join)
for c in (x.get("contents") or []):
if not isinstance(c, dict):
continue
role_raw = str(c.get("role") or "user")
role = "assistant" if role_raw == "model" else ("user" if role_raw not in {"user", "assistant"} else role_raw)
parts = c.get("parts") or []
text = "\n".join(
[str(p.get("text")) for p in parts if isinstance(p, dict) and isinstance(p.get("text"), str)]
).strip()
msgs.append({"role": role, "content": text})
return msgs
# List inputs
if isinstance(x, list):
# Gemini contents list -> OpenAI messages
if all(isinstance(c, dict) and "parts" in c for c in x):
for c in x:
role_raw = str(c.get("role") or "user")
role = "assistant" if role_raw == "model" else ("user" if role_raw not in {"user", "assistant"} else role_raw)
parts = c.get("parts") or []
text = "\n".join(
[str(p.get("text")) for p in parts if isinstance(p, dict) and isinstance(p.get("text"), str)]
).strip()
msgs.append({"role": role, "content": text})
return msgs
# OpenAI messages list already — normalize parts if needed
if all(isinstance(m, dict) and "content" in m for m in x):
out: List[Dict[str, Any]] = []
for m in x:
role = m.get("role", "user")
cont = m.get("content")
if isinstance(cont, str):
out.append({"role": role, "content": cont})
elif isinstance(cont, list):
parts2: List[Dict[str, Any]] = []
for p in cont:
if not isinstance(p, dict):
continue
if p.get("type") == "text":
parts2.append({"type": "text", "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:
parts2.append({"type": "image_url", "image_url": {"url": url}})
out.append({"role": role, "content": parts2 if parts2 else ""})
return out
# Fallback: dump JSON as a single user message
return [{"role": "user", "content": json.dumps(x, ensure_ascii=False)}]
# Primitive inputs or embedded JSON string
if isinstance(x, str):
try_obj = _try_json(x)
if try_obj is not None:
return self.normalize_segment(try_obj)
return [{"role": "user", "content": x}]
return [{"role": "user", "content": json.dumps(x, ensure_ascii=False)}]
except Exception:
return [{"role": "user", "content": str(x)}]
def filter_items(self, arr: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
"""
Совместимо с [_filter_openai()](agentui/pipeline/executor.py:2801).
"""
out: List[Dict[str, Any]] = []
for m in (arr or []):
if not isinstance(m, dict):
continue
c = m.get("content")
if isinstance(c, str) and c.strip():
out.append({"role": m.get("role", "user"), "content": c})
elif isinstance(c, list):
parts = []
for p in c:
if isinstance(p, dict) and p.get("type") == "text":
txt = str(p.get("text") or "")
if txt.strip():
parts.append({"type": "text", "text": txt})
if parts:
out.append({"role": m.get("role", "user"), "content": 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: systemInstruction.parts[].text
- Claude: top-level system (string/list of blocks)
- OpenAI: messages[*] with role=system (string content or parts[].text)
- List форматы: openai messages list и gemini contents list (в последнем случае смотрит incoming.json.systemInstruction)
"""
try:
# Dict objects
if isinstance(x, dict):
# Gemini systemInstruction
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()
# Claude system (string or blocks)
if "system" in x and not ("messages" in x and isinstance(x.get("messages"), list)):
sysv = x.get("system")
if isinstance(sysv, str) and sysv.strip():
return sysv.strip()
if isinstance(sysv, list):
texts = [
str(b.get("text") or "")
for b in sysv
if isinstance(b, dict) and (b.get("type") == "text") and isinstance(b.get("text"), str) and b.get("text").strip()
]
t = "\n".join([t for t in texts if t]).strip()
if t:
return t
# OpenAI messages with role=system
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 objects
if isinstance(x, list):
# OpenAI messages list with role=system
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: try to read systemInstruction from incoming JSON 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=='openai' из prompt_combine в [ProviderCallNode.run()](agentui/pipeline/executor.py:2936).
"""
built: List[Dict[str, Any]] = []
sys_texts: List[str] = []
# 1) Пред‑сегменты (prompt_preprocess)
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) Основные сегменты (prompt_combine)
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("messages", []) or []))
built = insert_items(built, items, pos_spec)
continue
# Спрятать дубли plain [[VAR:path]] если уже вставляли этим путём в pre_var_overrides
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:
# Уже вставлено через prompt_preprocess с фильтрацией — пропускаем
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=openai 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
# Если ничего не собрали — берём исходные blocks
if not built:
built = self.filter_items(list(blocks_struct.get("messages", []) or []))
# Препендинг системных сообщений из sys_texts
if sys_texts:
sys_msgs = [{"role": "system", "content": s} for s in sys_texts if s]
if sys_msgs:
built = sys_msgs + built
# keep system_text for UI/debug
st0 = blocks_struct.get("system_text") or ""
st = "\n\n".join([t for t in [st0] + sys_texts if isinstance(t, str) and t.strip()])
return {"messages": built, "system_text": st}
def prompt_fragment(self, pm_struct: Dict[str, Any], node_config: Dict[str, Any]) -> str:
"""
Совместимо с веткой provider=='openai' в построении [[PROMPT]] из [ProviderCallNode.run()](agentui/pipeline/executor.py:3103).
"""
return '"messages": ' + json.dumps(pm_struct.get("messages", []), ensure_ascii=False)

View File

@@ -0,0 +1,32 @@
from __future__ import annotations
from typing import Optional
from agentui.providers.adapters.base import ProviderAdapter, default_base_url_for as _default_base_url_for
from agentui.providers.adapters.openai import OpenAIAdapter
try:
from agentui.providers.adapters.gemini import GeminiAdapter, GeminiImageAdapter
except Exception:
GeminiAdapter = None # type: ignore
GeminiImageAdapter = None # type: ignore
try:
from agentui.providers.adapters.claude import ClaudeAdapter
except Exception:
ClaudeAdapter = None # type: ignore
def get_adapter(provider: str) -> Optional[ProviderAdapter]:
p = (provider or "").strip().lower()
if p == "openai":
return OpenAIAdapter()
if p == "gemini" and GeminiAdapter:
return GeminiAdapter() # type: ignore[operator]
if p == "gemini_image" and GeminiImageAdapter:
return GeminiImageAdapter() # type: ignore[operator]
if p == "claude" and ClaudeAdapter:
return ClaudeAdapter() # type: ignore[operator]
return None
def default_base_url_for(vendor: str) -> Optional[str]:
return _default_base_url_for(vendor)