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,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