@miller-tech/uap 1.84.1 → 1.85.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@miller-tech/uap",
3
- "version": "1.84.1",
3
+ "version": "1.85.0",
4
4
  "description": "Autonomous AI agent memory system with CLAUDE.md protocol enforcement",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -93,6 +93,11 @@ import sys
93
93
  import time
94
94
  import uuid
95
95
  from collections import OrderedDict, defaultdict, deque
96
+
97
+ try:
98
+ import confidence_escalation as _ce # serving-layer Confidence recipe
99
+ except Exception: # pragma: no cover - fail open if module missing
100
+ _ce = None
96
101
  from dataclasses import dataclass, field
97
102
  from pathlib import Path
98
103
 
@@ -2792,6 +2797,16 @@ def _should_use_guarded_non_stream(
2792
2797
  if PROXY_MALFORMED_TOOL_STREAM_STRICT and has_tools:
2793
2798
  return True
2794
2799
 
2800
+ # Confidence-escalation (vLLM "Confidence" recipe) needs the FULL answer
2801
+ # buffered to score it before deciding whether to escalate. Buffer single-
2802
+ # answer (no-tool) streaming turns when escalation is enabled. Default OFF.
2803
+ if not has_tools and _ce is not None:
2804
+ try:
2805
+ if _ce.Settings.from_env().enabled:
2806
+ return True
2807
+ except Exception:
2808
+ pass
2809
+
2795
2810
  # A2: when stream-passthrough is enabled, do NOT buffer required-tool turns
2796
2811
  # (native tool_choice='required' constrains them); force-non-stream and
2797
2812
  # malformed-strict above still apply.
@@ -7988,6 +8003,40 @@ def openai_to_anthropic_response(
7988
8003
  }
7989
8004
 
7990
8005
 
8006
+ async def _maybe_confidence_escalate(anthropic_resp, anthropic_body, client):
8007
+ """Confidence recipe: if the cheap primary answer scores below threshold,
8008
+ escalate the same (non-tool) turn to the stronger backend and return its
8009
+ answer. Default OFF; fails open (returns the original) on any problem."""
8010
+ if _ce is None or not isinstance(anthropic_resp, dict):
8011
+ return anthropic_resp
8012
+ try:
8013
+ settings = _ce.Settings.from_env()
8014
+ text = _ce.extract_text(anthropic_resp)
8015
+ if not _ce.should_escalate(text, settings, _has_tool_definitions(anthropic_body)):
8016
+ return anthropic_resp
8017
+ payload = _ce.build_escalation_payload(anthropic_body, settings)
8018
+ url = settings.endpoint.rstrip("/") + "/v1/messages"
8019
+ headers = {
8020
+ "Content-Type": "application/json",
8021
+ "x-api-key": settings.api_key,
8022
+ "anthropic-version": "2023-06-01",
8023
+ }
8024
+ resp = await client.post(url, json=payload, headers=headers, timeout=120.0)
8025
+ if resp.status_code == 200:
8026
+ logger.warning(
8027
+ "CONFIDENCE ESCALATION: primary confidence below %.2f -> escalated to %s",
8028
+ settings.threshold, settings.model,
8029
+ )
8030
+ return resp.json()
8031
+ logger.warning(
8032
+ "CONFIDENCE ESCALATION: backend %s returned %d; keeping primary",
8033
+ settings.model, resp.status_code,
8034
+ )
8035
+ except Exception as exc:
8036
+ logger.warning("CONFIDENCE ESCALATION: failed (%s); keeping primary", exc)
8037
+ return anthropic_resp
8038
+
8039
+
7991
8040
  async def _heartbeat_then_buffered(produce_coro, model: str):
7992
8041
  """SSE generator: keep-alive heartbeat wrapper for the guarded-non-stream path.
7993
8042
 
@@ -8953,6 +9002,7 @@ async def messages(request: Request):
8953
9002
  "REQUIRED TOOL STREAM GUARDRAIL: served stream response via guarded non-stream path"
8954
9003
  )
8955
9004
 
9005
+ anthropic_resp = await _maybe_confidence_escalate(anthropic_resp, body, client)
8956
9006
  return anthropic_resp
8957
9007
 
8958
9008
  if PROXY_STREAM_HEARTBEAT_SECS > 0:
@@ -0,0 +1,104 @@
1
+ """Confidence-escalation looper (vLLM Semantic Router "Confidence" recipe).
2
+
3
+ Try the cheap primary model first; if a confidence signal on its answer is below
4
+ threshold, escalate the SAME request to a stronger backend and return that
5
+ answer instead. Bounded (one escalation), default OFF, fails open. Scoped to
6
+ non-tool (single-answer) turns — escalating a mid-loop tool turn would need the
7
+ stronger model to share the whole tool/context state, which is out of scope for
8
+ this primitive.
9
+
10
+ The confidence signal is intentionally pluggable. This module ships a cheap
11
+ text heuristic; a stronger signal (token logprob margin, or UAP's real
12
+ execution/acceptance GATE-pass — the differentiator vs logprob) can replace
13
+ ``text_confidence`` without touching the proxy hook.
14
+
15
+ All config is env-driven and OFF by default:
16
+ PROXY_CONFIDENCE_ESCALATE=on enable the looper
17
+ PROXY_CONFIDENCE_THRESHOLD=0.5 escalate when confidence < this
18
+ PROXY_ESCALATE_MODEL=<id> model id sent to the escalation backend
19
+ PROXY_ESCALATE_ENDPOINT=<url> Anthropic-compatible /v1/messages base
20
+ PROXY_ESCALATE_API_KEY=<key> x-api-key for the escalation backend
21
+ """
22
+ from __future__ import annotations
23
+
24
+ import os
25
+ import re
26
+ from dataclasses import dataclass
27
+
28
+
29
+ @dataclass
30
+ class Settings:
31
+ enabled: bool
32
+ threshold: float
33
+ model: str
34
+ endpoint: str
35
+ api_key: str
36
+
37
+ @classmethod
38
+ def from_env(cls) -> "Settings":
39
+ on = os.environ.get("PROXY_CONFIDENCE_ESCALATE", "off").lower() not in {
40
+ "", "0", "off", "false", "no",
41
+ }
42
+ try:
43
+ thr = float(os.environ.get("PROXY_CONFIDENCE_THRESHOLD", "0.5"))
44
+ except ValueError:
45
+ thr = 0.5
46
+ return cls(
47
+ enabled=on,
48
+ threshold=thr,
49
+ model=os.environ.get("PROXY_ESCALATE_MODEL", ""),
50
+ endpoint=os.environ.get("PROXY_ESCALATE_ENDPOINT", ""),
51
+ api_key=os.environ.get("PROXY_ESCALATE_API_KEY", ""),
52
+ )
53
+
54
+ def backend_configured(self) -> bool:
55
+ return bool(self.model and self.endpoint)
56
+
57
+
58
+ _UNCERTAIN = re.compile(
59
+ r"\b(i'?m not sure|i am not sure|i don'?t know|i cannot|i can'?t (?:help|do|determine)|"
60
+ r"unable to|not certain|no idea|as an ai|i'?m sorry,? but)\b",
61
+ re.I,
62
+ )
63
+
64
+
65
+ def text_confidence(text: str) -> float:
66
+ """Cheap heuristic confidence in [0,1]. Conservative: only clearly weak
67
+ answers (empty, refusal/uncertainty, trivially short) score low."""
68
+ t = (text or "").strip()
69
+ if not t:
70
+ return 0.0
71
+ if _UNCERTAIN.search(t):
72
+ return 0.2
73
+ if len(t) < 20:
74
+ return 0.3
75
+ return 0.9
76
+
77
+
78
+ def extract_text(anthropic_resp: dict) -> str:
79
+ """Concatenate text blocks of an Anthropic message response."""
80
+ parts = []
81
+ for blk in (anthropic_resp or {}).get("content", []) or []:
82
+ if isinstance(blk, dict) and blk.get("type") == "text":
83
+ parts.append(blk.get("text", ""))
84
+ return "".join(parts)
85
+
86
+
87
+ def should_escalate(text: str, settings: Settings, has_tools: bool) -> bool:
88
+ if not settings.enabled or not settings.backend_configured():
89
+ return False
90
+ if has_tools: # single-answer scope only
91
+ return False
92
+ return text_confidence(text) < settings.threshold
93
+
94
+
95
+ def build_escalation_payload(anthropic_body: dict, settings: Settings) -> dict:
96
+ """Re-issue the same conversation to the stronger model (no tools)."""
97
+ payload = {
98
+ "model": settings.model,
99
+ "max_tokens": anthropic_body.get("max_tokens", 4096),
100
+ "messages": anthropic_body.get("messages", []),
101
+ }
102
+ if anthropic_body.get("system"):
103
+ payload["system"] = anthropic_body["system"]
104
+ return payload
@@ -0,0 +1,56 @@
1
+ """Tests for the confidence-escalation looper (vLLM Confidence recipe)."""
2
+ import importlib.util
3
+ import unittest
4
+ from pathlib import Path
5
+
6
+ mod_path = Path(__file__).resolve().parents[3] / "tools" / "agents" / "scripts" / "confidence_escalation.py"
7
+ spec = importlib.util.spec_from_file_location("confidence_escalation", mod_path)
8
+ ce = importlib.util.module_from_spec(spec)
9
+ import sys as _sys; _sys.modules["confidence_escalation"]=ce; spec.loader.exec_module(ce)
10
+
11
+
12
+ def S(enabled=True, threshold=0.5, model="opus", endpoint="http://x/", api_key="k"):
13
+ return ce.Settings(enabled=enabled, threshold=threshold, model=model, endpoint=endpoint, api_key=api_key)
14
+
15
+
16
+ class ConfidenceTest(unittest.TestCase):
17
+ def test_text_confidence_levels(self):
18
+ self.assertEqual(ce.text_confidence(""), 0.0)
19
+ self.assertEqual(ce.text_confidence("I don't know"), 0.2)
20
+ self.assertEqual(ce.text_confidence("ok"), 0.3)
21
+ self.assertGreater(ce.text_confidence("Here is a complete, detailed answer to your question."), 0.5)
22
+
23
+ def test_extract_text(self):
24
+ resp = {"content": [{"type": "text", "text": "a"}, {"type": "tool_use"}, {"type": "text", "text": "b"}]}
25
+ self.assertEqual(ce.extract_text(resp), "ab")
26
+
27
+
28
+ class ShouldEscalateTest(unittest.TestCase):
29
+ def test_low_confidence_escalates(self):
30
+ self.assertTrue(ce.should_escalate("I cannot help", S(), has_tools=False))
31
+
32
+ def test_high_confidence_does_not(self):
33
+ self.assertFalse(ce.should_escalate("A thorough and confident full answer here.", S(), has_tools=False))
34
+
35
+ def test_disabled_never(self):
36
+ self.assertFalse(ce.should_escalate("", S(enabled=False), has_tools=False))
37
+
38
+ def test_no_backend_never(self):
39
+ self.assertFalse(ce.should_escalate("", S(endpoint=""), has_tools=False))
40
+
41
+ def test_tool_turn_never(self):
42
+ self.assertFalse(ce.should_escalate("", S(), has_tools=True))
43
+
44
+
45
+ class PayloadTest(unittest.TestCase):
46
+ def test_payload_uses_escalation_model_and_messages(self):
47
+ body = {"model": "qwen", "max_tokens": 100, "system": "sys", "messages": [{"role": "user", "content": "hi"}]}
48
+ p = ce.build_escalation_payload(body, S(model="opus"))
49
+ self.assertEqual(p["model"], "opus")
50
+ self.assertEqual(p["messages"], body["messages"])
51
+ self.assertEqual(p["system"], "sys")
52
+ self.assertNotIn("tools", p)
53
+
54
+
55
+ if __name__ == "__main__":
56
+ unittest.main()