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