Ctrl K
ComunidadGuías…

agent/reasoning_carriers.py

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
"""Provider-native assistant reasoning carriers outside the standard reasoning fields.

* Gemini text-turn ``thought_signature`` (message-level ``extra_content.google`` on the OpenAI-compat
  and Vertex endpoints; the non-functionCall ``thoughtSignature`` part on the native API). Replaying it
  makes the server rehydrate the turn's thoughts (live: 31 -> 237/250 prompt tokens).
* Top-level assistant keys a provider profile declares (``ProviderProfile.message_carrier_opaque`` /
  ``message_carrier_text``: an opaque reasoning blob and its readable text), plus the tool-call
  ``function.thought_signature`` some gateways sign Gemini 3 calls with.

Capture stores them top-level on the live assistant dict AND as ONE private ``reasoning_details`` record,
so they persist in the existing column and survive gateway/resume replay. Its type ends in ``.native_assistant``, which no profile declares, so
the chat-completions transport never forwards the record itself; ``shape_wire_carriers`` lifts each
field back onto the wire copy only for the route that reads it (the endpoint profile's
``replay_message_carriers``) and strips it everywhere else, including keys of records whose declaring
plugin is no longer installed.
"""

from __future__ import annotations

from typing import Any, NamedTuple, Optional

CARRIER_TYPE = "hermes.native_assistant"
_GEMINI_KEY = "extra_content"


def declared_carrier_keys() -> tuple[tuple[str, ...], tuple[str, ...]]:
    """``(opaque keys, text keys)`` declared by the registered profiles, in registration order. Call once per
    message/stream/request, never per delta."""
    from providers import list_providers

    profiles = list_providers()
    opaque = (p.message_carrier_opaque for p in profiles if p.message_carrier_opaque)
    text = (p.message_carrier_text for p in profiles if p.message_carrier_text)
    return tuple(dict.fromkeys(opaque)), tuple(dict.fromkeys(text))


def _message_keys() -> tuple[str, ...]:
    opaque, text = declared_carrier_keys()
    return (_GEMINI_KEY, *dict.fromkeys(opaque + text))


def field(obj: Any, name: str) -> Any:
    """``obj.<name>``, else the same key from pydantic ``model_extra`` (unknown SDK fields park there)."""
    value = getattr(obj, name, None)
    for side in ("model_extra", "provider_data"):  # pydantic unknown fields / normalized responses
        if value is None and isinstance(getattr(obj, side, None), dict):
            value = getattr(obj, side).get(name)
    dump = getattr(value, "model_dump", None)
    return dump() if callable(dump) else value


def thought_signature(extra: Any) -> Optional[str]:
    """Gemini signature inside an ``extra_content`` dict (``google.thought_signature`` or flat)."""
    if not isinstance(extra, dict):
        return None
    google = extra.get("google")
    sig = google.get("thought_signature") if isinstance(google, dict) else extra.get("thought_signature")
    return sig if isinstance(sig, str) and sig.strip() else None


def carrier_record(assistant_message: Any) -> Optional[dict]:
    """The private ``reasoning_details`` record for one response message, or None when it has no carrier."""
    record: dict[str, Any] = {}
    if sig := thought_signature(field(assistant_message, _GEMINI_KEY)):
        record[_GEMINI_KEY] = {"google": {"thought_signature": sig}}
    for key in _message_keys()[1:]:
        value = field(assistant_message, key)
        if isinstance(value, str) and value:
            record[key] = value
    return {"type": CARRIER_TYPE, **record} if record else None


class StreamCarriers:
    """Accumulates the carriers from streamed deltas: the latest signature / opaque blob per declared key (a
    provider may send a new opaque before each tool call and expect only the latest back) and the concatenated
    text of each declared text key. Keys are resolved once per stream."""

    def __init__(self) -> None:
        self.opaque_keys, self.text_keys = declared_carrier_keys()
        self.signature: Optional[str] = None
        self.opaque: dict[str, str] = {}
        self.text_parts: dict[str, list[str]] = {}

    def feed(self, delta: Any, reasoning: Optional[str]) -> Optional[str]:
        """Absorb one delta; returns ``reasoning``, else its first declared text-carrier fragment (readable reasoning)."""
        self.signature = thought_signature(field(delta, _GEMINI_KEY)) or self.signature
        for key in self.opaque_keys:
            opaque = field(delta, key)
            if isinstance(opaque, str) and opaque:
                self.opaque[key] = opaque
        for key in self.text_keys:
            text = field(delta, key)
            if isinstance(text, str) and text:
                self.text_parts.setdefault(key, []).append(text)
                reasoning = text if reasoning is None else reasoning
        return reasoning

    def apply(self, response: Any) -> None:
        choices = getattr(response, "choices", None)
        message = getattr(choices[0], "message", None) if choices else None
        if message is None:
            return
        if self.signature:
            message.extra_content = {"google": {"thought_signature": self.signature}}
        for key, value in self.opaque.items():
            setattr(message, key, value)
        for key, parts in self.text_parts.items():
            setattr(message, key, "".join(parts))


class _WireRoute(NamedTuple):
    model: Any
    base_url: str
    gemini: bool  # the route consumes the Gemini text-turn signature
    profile: Any  # profile serving the host (``providers.profile_for_host``) or None
    keep_fn_sig: bool  # replayed tool calls keep ``function.thought_signature``
    keys: tuple[str, ...]  # every message-level carrier key core knows about
    scope: Any  # who remembers a failed hook (``provider_route_hooks.ask_profile``): the agent, or None


def _wire_route(model: Any, base_url: Any, scope: Any = None) -> _WireRoute:
    from providers import profile_for_host
    from agent.provider_route_hooks import ask_profile
    from agent.transports.chat_completions import _model_consumes_thought_signature

    url = str(base_url or "")
    profile = profile_for_host(url)
    keep_fn_sig = profile is not None and bool(ask_profile(
        profile, "replays_function_thought_signature", False, model=model, base_url=url, scope=scope))
    # Same gate as tool-call extra_content, so the two signature carriers can never disagree.
    return _WireRoute(model, url, _model_consumes_thought_signature(model), profile, keep_fn_sig, _message_keys(),
                      scope)


# ``replay_message_carriers`` answers by which carrier keys are present (providers/base.py), so its answer
# for one key set is kept as a key mapping and applied to every message with that key set: one hook call
# per (route, key set) instead of one per carrier-bearing message per request.
_REPLAY_PLANS: dict[tuple, Any] = {}
_REPLAY_PLANS_MAX = 512
_REWRITES = object()  # the hook rewrites values: ask it per message
_FAILED = object()


def _replay_plan(answer: Any, values: dict) -> Any:
    """``((out key, source key), ...)`` reproducing *answer* from *values*; None when ambiguous."""
    if not isinstance(answer, dict):
        return _REWRITES
    plan = []
    for out_key, out_value in answer.items():
        sources = [k for k, v in values.items() if v == out_value]
        if not sources:
            return _REWRITES
        if len(sources) > 1:
            return None
        plan.append((out_key, sources[0]))
    return tuple(plan)


def _replay_carriers(route: _WireRoute, values: dict) -> dict:
    from providers import registry_generation
    from agent.provider_route_hooks import ask_profile, hook_failed

    if hook_failed(route.profile, "replay_message_carriers", route.scope):
        return {}
    key = (registry_generation(), id(route.profile), type(route.profile), route.model, route.base_url,
           tuple(sorted(values)))
    plan = _REPLAY_PLANS.get(key)
    if plan is not None and plan is not _REWRITES:
        return {out: values[src] for out, src in plan}
    answer = ask_profile(route.profile, "replay_message_carriers", _FAILED, values, model=route.model,
                         base_url=route.base_url, scope=route.scope)
    if answer is _FAILED:
        return {}
    if plan is None and (plan := _replay_plan(answer, values)) is not None:
        if len(_REPLAY_PLANS) >= _REPLAY_PLANS_MAX:
            _REPLAY_PLANS.clear()
        _REPLAY_PLANS[key] = plan
    return dict(answer or {})


def _strip_function_signature(tool_calls: list) -> list:
    return [
        {**tc, "function": {k: v for k, v in tc["function"].items() if k != "thought_signature"}}
        if isinstance(tc, dict) and isinstance(tc.get("function"), dict) and "thought_signature" in tc["function"]
        else tc
        for tc in tool_calls
    ]


def _shape(msg: Any, route: _WireRoute) -> Any:
    if not isinstance(msg, dict) or msg.get("role") != "assistant":
        return msg
    details = msg.get("reasoning_details")
    details = details if isinstance(details, list) else []
    record = next((d for d in details if isinstance(d, dict) and d.get("type") == CARRIER_TYPE), None)
    tool_calls = msg.get("tool_calls")
    tool_calls = tool_calls if isinstance(tool_calls, list) else []
    fn_sig = not route.keep_fn_sig and any(
        isinstance(tc, dict) and isinstance(tc.get("function"), dict) and "thought_signature" in tc["function"]
        for tc in tool_calls)
    # A persisted record's own keys are stripped too, so rows from an uninstalled provider plugin never leak.
    keys = route.keys if record is None else (*route.keys, *(k for k in record if k != "type" and k not in route.keys))
    if record is None and not fn_sig and not any(k in msg for k in keys):
        return msg
    out = {k: v for k, v in msg.items() if k not in keys}
    # Live history carries the keys top-level; a resumed row only has the persisted record.
    values = {**(record or {}), **{k: msg[k] for k in keys if k in msg}}
    if record is not None:
        rest = [d for d in details if d is not record]
        if rest:
            out["reasoning_details"] = rest
        else:
            out.pop("reasoning_details", None)
    if route.gemini and values.get(_GEMINI_KEY):
        out[_GEMINI_KEY] = values[_GEMINI_KEY]
    if route.profile is not None:
        out.update(_replay_carriers(route, values))
    if fn_sig:
        out["tool_calls"] = _strip_function_signature(tool_calls)
    return out


def shape_wire_carriers(messages: list, *, model: Any, base_url: Any, scope: Any = None) -> list:
    """Wire copy of ``messages`` with each carrier on the route that reads it and nowhere else.
    Never mutates the input; unchanged messages are returned by identity. *scope* is the agent (failed
    profile hooks stay failed for it; see ``provider_route_hooks.ask_profile``)."""
    from agent.provider_route_hooks import hook_failed

    route = _wire_route(model, base_url, scope)
    failed_before = route.profile is None or hook_failed(route.profile, "replay_message_carriers", scope)
    shaped = [_shape(m, route) for m in messages]
    if not failed_before and hook_failed(route.profile, "replay_message_carriers", scope):
        # Failed mid-request: reshape so every message takes the fallback, as every later request will.
        shaped = [_shape(m, route) for m in messages]
    return messages if all(a is b for a, b in zip(shaped, messages)) else shaped