Source code for mixle.substrate.interop

"""External model interop for reasoner delegation.

:class:`ExternalModel` wraps a ``generate`` callable from an external model,
agent, hosted LLM, or remote tool. It can estimate semantic uncertainty by
sampling multiple answers and clustering them through an equivalence function.

:func:`external_action` adapts the wrapper into a reasoner
:class:`~mixle.substrate.act.Action`. When the external model is above its
uncertainty cutoff, the action contributes no evidence, allowing the reasoner
to continue or abstain.
"""

from __future__ import annotations

from collections.abc import Callable
from dataclasses import dataclass
from typing import Any


[docs] @dataclass class ExternalAnswer: """An external model's answer plus its self-measured uncertainty (semantic entropy).""" prompt: Any answer: Any entropy: float confident: bool
[docs] class ExternalModel: """An external ``generate`` callable wrapped so each answer carries semantic-entropy UQ. Args: generate: ``prompt -> answer`` (an external agent / LLM / remote tool). Called multiple times per query to measure how much its meaning varies (the uncertainty signal). calibration_prompts: optional example prompts; the (1-alpha) quantile of their semantic entropy becomes the "too uncertain" cutoff. Without them, ``max_entropy`` must be given (or every answer is treated as confident). equivalent: ``(a, b) -> bool`` meaning-equivalence for clustering samples (default: exact match). max_entropy: an explicit uncertainty cutoff, overriding the calibrated one. samples: how many resamples to draw when measuring entropy. """ def __init__( self, generate: Callable[[Any], Any], *, calibration_prompts: Any = None, equivalent: Callable[[Any, Any], bool] | None = None, max_entropy: float | None = None, alpha: float = 0.1, samples: int = 8, ) -> None: from mixle.inference.uq import uq self.generate = generate self.samples = int(samples) self._uq = uq(generate, calibration_prompts, alpha=alpha, equivalent=equivalent) if max_entropy is not None: self._uq.payload["max_entropy"] = float(max_entropy) @property def max_entropy(self) -> float: return float(self._uq.payload.get("max_entropy", float("inf")))
[docs] def answer(self, prompt: Any) -> ExternalAnswer: """Call the external model and attach its semantic-entropy UQ (confident iff below the cutoff).""" text = self.generate(prompt) entropy = self._uq.semantic_entropy(prompt, n=self.samples) return ExternalAnswer( prompt=prompt, answer=text, entropy=float(entropy), confident=entropy <= self.max_entropy, )
[docs] def confident(self, prompt: Any) -> bool: return self._uq.confident(prompt, n=self.samples)
[docs] def external_action( model: ExternalModel, *, name: str = "external", cost: float = 8.0, description: str = "", trust_uncertain: bool = False, ) -> Any: """A reasoner DELEGATE action backed by a UQ-wrapped external model (see module docstring). By default (``trust_uncertain=False``) the action contributes evidence ONLY when the external model is confident about the query; an uncertain external answer yields no fragment, so the reasoner treats it as no answer rather than a guess. The fragment carries the model's entropy so the trace records how sure the external source was. Cost defaults high -- external calls are the escalation of last resort.""" from mixle.substrate.act import Action def _run(question: str) -> list[str]: result = model.answer(question) if not result.confident and not trust_uncertain: return [] # self-contradicting external answer -> withhold, don't fabricate confidence tag = "confident" if result.confident else "uncertain" return [f"external[{tag}, entropy={result.entropy:.3f}] => {result.answer}"] return Action(name=name, kind="delegate", run=_run, cost=cost, description=description)