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:;base64, """ 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)