148 lines
6.1 KiB
Python
148 lines
6.1 KiB
Python
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 |