orchestrata_ultimate.orchestrator.ambiguity_resolver

Reference › orchestrata_ultimate › orchestrator › ambiguity_resolver

Decides between competing readings of the same intent. No other reference page imports it. It is traced to spec 003 story US-001 (REQ-003-US-001-007, 017, 018 and 032).

An AmbiguityCandidate is an action, a target, a confidence and optional metadata. AmbiguityResolver.detect_ambiguity returns an AmbiguityResult: with no candidates the reason is no_candidates; with one it is resolved immediately; otherwise _score_candidates scores them against the text, sorts by confidence, and the intent is ambiguous when the gap between the top two is below AMBIGUITY_THRESHOLD. resolve goes further for an ambiguous case. It first tries Prolog rules through an optional bridge (_try_prolog_resolution, reason prolog_resolved), then a context dictionary (_try_context_resolution, reason context_resolved), and finally falls back to the highest-scoring candidate. AmbiguityResult.confidence exposes the confidence of the resolved candidate.

At a glance

Source src/orchestrata_ultimate/orchestrator/ambiguity_resolver.py
Lines of code 261
Classes 3
Functions 0
Imports (internal) orchestrata_ultimate.core.base.traces
Imported by none
Tests that import it tests/generated_orchestrator/test_ambiguity_resolver_generated.py, tests/orchestrator/test_ambiguity_resolver.py, tests/orchestrator/test_intent_logic_behavioral.py, tests/test_orchestrator.py

Class diagram

Class diagram orchestrata_ultimate.orchestrator.ambiguity_resolver

Dependency diagram

Dependency diagram orchestrata_ultimate.orchestrator.ambiguity_resolver

API

Classes

AmbiguityCandidate

Decorators: traces(spec='003', story='US-001', reqs=['REQ-003-US-001-017']) · Line 18

A candidate interpretation of an ambiguous intent.

Method Signature Summary
__init__ __init__(action: str, target: str, confidence: float = 0.0, metadata: dict | None = None) -> None
to_dict to_dict() -> dict[str, Any]

AmbiguityResult

Decorators: traces(spec='003', story='US-001', reqs=['REQ-003-US-001-032']) · Line 45

Result of ambiguity detection/resolution.

Method Signature Summary
__init__ __init__(is_ambiguous: bool, candidates: list[AmbiguityCandidate], resolved: AmbiguityCandidate | None = None, reason: str = '') -> None
confidence property confidence() -> float

AmbiguityResolver

Decorators: traces(spec='003', story='US-001', reqs=['REQ-003-US-001-007', 'REQ-003-US-001-017', 'REQ-003-US-001-018', 'REQ-003-US-… · Line 80

Detects and resolves ambiguous intent interpretations.

Method Signature Summary
__init__ __init__(prolog_bridge: Any = None) -> None
detect_ambiguity detect_ambiguity(text: str, candidates: list[AmbiguityCandidate]) -> AmbiguityResult Detect if candidates represent an ambiguous intent.
resolve resolve(text: str, candidates: list[AmbiguityCandidate], context: dict | None = None) -> AmbiguityResult Resolve ambiguity using scoring and optional Prolog rules.
_score_candidates _score_candidates(text: str, candidates: list[AmbiguityCandidate]) -> list[AmbiguityCandidate] Score candidates based on text similarity.
_try_prolog_resolution _try_prolog_resolution(text: str, candidates: list[AmbiguityCandidate], context: dict | None) -> AmbiguityCandidate | None Try resolving via Prolog rules.
_try_context_resolution _try_context_resolution(candidates: list[AmbiguityCandidate], context: dict) -> AmbiguityCandidate | None Try resolving using context hints.

Source

Permalink: https://gitlab.com/MSamy/orchestrata-ultimate-architecture/-/blob/a4cbc2bdf/src/orchestrata_ultimate/orchestrator/ambiguity_resolver.py

"""Ambiguity detection and resolution for intents.

Detects when an intent maps to multiple valid interpretations
and resolves via scoring and optional Prolog rules.
"""

# Spec: 003 - Intent Broker

import logging
from typing import Any

from orchestrata_ultimate.core.base.traces import traces

logger = logging.getLogger(__name__)


@traces(spec="003", story="US-001", reqs=["REQ-003-US-001-017"])
class AmbiguityCandidate:
    """A candidate interpretation of an ambiguous intent."""

    @traces(spec="003", story="US-001", reqs=["REQ-003-US-001-032"])
    def __init__(
        self,
        action: str,
        target: str,
        confidence: float = 0.0,
        metadata: dict | None = None,
    ) -> None:
        self.action = action
        self.target = target
        self.confidence = confidence
        self.metadata = metadata or {}

    @traces(spec="003", story="US-001", reqs=["REQ-003-US-001-032"])
    def to_dict(self) -> dict[str, Any]:
        return {
            "action": self.action,
            "target": self.target,
            "confidence": self.confidence,
            "metadata": self.metadata,
        }


@traces(spec="003", story="US-001", reqs=["REQ-003-US-001-032"])
class AmbiguityResult:
    """Result of ambiguity detection/resolution."""

    @traces(spec="003", story="US-001", reqs=["REQ-003-US-001-032"])
    def __init__(
        self,
        is_ambiguous: bool,
        candidates: list[AmbiguityCandidate],
        resolved: AmbiguityCandidate | None = None,
        reason: str = "",
    ) -> None:
        self.is_ambiguous = is_ambiguous
        self.candidates = candidates
        self.resolved = resolved
        self.reason = reason

    @property
    @traces(spec="003", story="US-001", reqs=["REQ-003-US-001-017"])
    def confidence(self) -> float:
        if self.resolved:
            return self.resolved.confidence
        return 0.0


@traces(
    spec="003",
    story="US-001",
    reqs=[
        "REQ-003-US-001-007",
        "REQ-003-US-001-017",
        "REQ-003-US-001-018",
        "REQ-003-US-001-031",
        "REQ-003-US-001-032",
    ],
)
class AmbiguityResolver:
    """Detects and resolves ambiguous intent interpretations."""

    AMBIGUITY_THRESHOLD = 0.3  # Max gap between top two for ambiguity

    @traces(spec="003", story="US-001", reqs=["REQ-003-US-001-017"])
    def __init__(self, prolog_bridge: Any = None) -> None:
        self._prolog = prolog_bridge

    @traces(
        spec="003",
        story="US-001",
        reqs=["REQ-003-US-001-007", "REQ-003-US-001-017", "REQ-003-US-001-032"],
    )
    def detect_ambiguity(
        self, text: str, candidates: list[AmbiguityCandidate]
    ) -> AmbiguityResult:
        """Detect if candidates represent an ambiguous intent."""
        if not candidates:
            return AmbiguityResult(
                is_ambiguous=False,
                candidates=[],
                reason="no_candidates",
            )

        if len(candidates) == 1:
            return AmbiguityResult(
                is_ambiguous=False,
                candidates=candidates,
                resolved=candidates[0],
                reason="single_candidate",
            )

        # Score all candidates
        scored = self._score_candidates(text, candidates)

        # Sort by confidence descending
        scored.sort(key=lambda c: c.confidence, reverse=True)

        # Check if top two are too close
        gap = scored[0].confidence - scored[1].confidence
        is_ambiguous = gap < self.AMBIGUITY_THRESHOLD

        return AmbiguityResult(
            is_ambiguous=is_ambiguous,
            candidates=scored,
            resolved=scored[0] if not is_ambiguous else None,
            reason="ambiguous" if is_ambiguous else "clear_winner",
        )

    @traces(
        spec="003",
        story="US-001",
        reqs=["REQ-003-US-001-007", "REQ-003-US-001-018", "REQ-003-US-001-031"],
    )
    def resolve(
        self,
        text: str,
        candidates: list[AmbiguityCandidate],
        context: dict | None = None,
    ) -> AmbiguityResult:
        """Resolve ambiguity using scoring and optional Prolog rules."""
        if not candidates:
            return AmbiguityResult(
                is_ambiguous=False,
                candidates=[],
                reason="no_candidates",
            )

        detection = self.detect_ambiguity(text, candidates)

        if not detection.is_ambiguous:
            return detection

        # Try Prolog resolution
        if self._prolog is not None:
            prolog_result = self._try_prolog_resolution(text, candidates, context)
            if prolog_result is not None:
                return AmbiguityResult(
                    is_ambiguous=False,
                    candidates=detection.candidates,
                    resolved=prolog_result,
                    reason="prolog_resolved",
                )

        # Context-based resolution
        if context:
            context_result = self._try_context_resolution(candidates, context)
            if context_result is not None:
                return AmbiguityResult(
                    is_ambiguous=False,
                    candidates=detection.candidates,
                    resolved=context_result,
                    reason="context_resolved",
                )

        # Fall back to highest scoring
        best = detection.candidates[0]
        return AmbiguityResult(
            is_ambiguous=True,
            candidates=detection.candidates,
            resolved=best,
            reason="best_effort",
        )

    @traces(spec="003", story="US-001", reqs=["REQ-003-US-001-017"])
    def _score_candidates(
        self, text: str, candidates: list[AmbiguityCandidate]
    ) -> list[AmbiguityCandidate]:
        """Score candidates based on text similarity."""
        text_lower = text.lower()
        text_words = set(text_lower.split())

        for candidate in candidates:
            score = candidate.confidence  # Start with existing confidence

            # Action word match
            if candidate.action.lower() in text_lower:
                score += 0.4

            # Target word match
            if candidate.target.lower() in text_lower:
                score += 0.3

            # Word overlap
            action_words = set(candidate.action.lower().split("_"))
            target_words = set(candidate.target.lower().split("_"))
            all_candidate_words = action_words | target_words
            overlap = text_words & all_candidate_words
            if all_candidate_words:
                score += len(overlap) / len(all_candidate_words) * 0.3

            candidate.confidence = round(score, 4)

        return candidates

    @traces(spec="003", story="US-003", reqs=["REQ-003-US-003-012"])
    def _try_prolog_resolution(
        self, text: str, candidates: list[AmbiguityCandidate], context: dict | None
    ) -> AmbiguityCandidate | None:
        """Try resolving via Prolog rules."""
        try:
            for candidate in candidates:
                query = f"resolve_ambiguity('{candidate.action}', '{candidate.target}', Priority)"
                result = self._prolog.query(query)
                if result.success and result.results:
                    priority = result.results[0].get("Priority", 0)
                    candidate.confidence += float(priority) * 0.1
                    candidate.metadata["prolog_priority"] = priority

            # Re-sort after Prolog scoring
            candidates.sort(key=lambda c: c.confidence, reverse=True)
            if len(candidates) >= 2:
                gap = candidates[0].confidence - candidates[1].confidence
                if gap >= self.AMBIGUITY_THRESHOLD:
                    return candidates[0]
        except Exception as e:
            logger.warning("Prolog resolution failed: %s", e)

        return None

    @traces(spec="003", story="US-001", reqs=["REQ-003-US-001-020"])
    def _try_context_resolution(
        self, candidates: list[AmbiguityCandidate], context: dict
    ) -> AmbiguityCandidate | None:
        """Try resolving using context hints."""
        preferred_action = context.get("preferred_action")
        preferred_target = context.get("preferred_target")

        for candidate in candidates:
            if preferred_action and candidate.action == preferred_action:
                candidate.confidence += 0.5
            if preferred_target and candidate.target == preferred_target:
                candidate.confidence += 0.5

        candidates.sort(key=lambda c: c.confidence, reverse=True)
        if len(candidates) >= 2:
            gap = candidates[0].confidence - candidates[1].confidence
            if gap >= self.AMBIGUITY_THRESHOLD:
                return candidates[0]

        return None