@miller-tech/uap 1.84.1 → 1.86.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.86.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,70 @@ def openai_to_anthropic_response(
7988
8003
  }
7989
8004
 
7990
8005
 
8006
+ async def _maybe_apply_recipe(anthropic_resp, anthropic_body, openai_body, client):
8007
+ """Serving-layer recipe runtime (confidence / fusion, signal-selected).
8008
+ Default OFF; fails open. call_primary re-runs the cheap primary (llama) for
8009
+ fusion breadth; call_judge talks to the stronger escalation backend."""
8010
+ if _ce is None or not isinstance(anthropic_resp, dict):
8011
+ return anthropic_resp
8012
+ try:
8013
+ settings = _ce.Settings.from_env()
8014
+ if not settings.enabled:
8015
+ return anthropic_resp
8016
+ model_name = (openai_body or {}).get("model", "default")
8017
+
8018
+ async def call_primary(openai_variant):
8019
+ try:
8020
+ r = await _post_with_generation_timeout(
8021
+ client,
8022
+ f"{LLAMA_CPP_BASE}/chat/completions",
8023
+ openai_variant,
8024
+ {"Content-Type": "application/json"},
8025
+ )
8026
+ if getattr(r, "status_code", 0) != 200:
8027
+ return None
8028
+ return openai_to_anthropic_response(r.json(), model_name)
8029
+ except Exception:
8030
+ return None
8031
+
8032
+ async def call_judge(anthropic_payload):
8033
+ try:
8034
+ url = settings.endpoint.rstrip("/") + "/v1/messages"
8035
+ r = await client.post(
8036
+ url,
8037
+ json=anthropic_payload,
8038
+ headers={
8039
+ "Content-Type": "application/json",
8040
+ "x-api-key": settings.api_key,
8041
+ "anthropic-version": "2023-06-01",
8042
+ },
8043
+ timeout=120.0,
8044
+ )
8045
+ return r.json() if getattr(r, "status_code", 0) == 200 else None
8046
+ except Exception:
8047
+ return None
8048
+
8049
+ result = await _ce.apply_recipe(
8050
+ anthropic_resp,
8051
+ anthropic_body,
8052
+ openai_body,
8053
+ settings,
8054
+ _has_tool_definitions(anthropic_body),
8055
+ call_primary,
8056
+ call_judge,
8057
+ )
8058
+ if isinstance(result, dict) and result is not anthropic_resp:
8059
+ logger.warning(
8060
+ "RECIPE applied: recipe=%s signal=%s -> response changed",
8061
+ _ce.select_recipe(anthropic_body, settings, _has_tool_definitions(anthropic_body)),
8062
+ settings.signal,
8063
+ )
8064
+ return result if isinstance(result, dict) else anthropic_resp
8065
+ except Exception as exc:
8066
+ logger.warning("RECIPE: failed (%s); keeping primary answer", exc)
8067
+ return anthropic_resp
8068
+
8069
+
7991
8070
  async def _heartbeat_then_buffered(produce_coro, model: str):
7992
8071
  """SSE generator: keep-alive heartbeat wrapper for the guarded-non-stream path.
7993
8072
 
@@ -8953,6 +9032,7 @@ async def messages(request: Request):
8953
9032
  "REQUIRED TOOL STREAM GUARDRAIL: served stream response via guarded non-stream path"
8954
9033
  )
8955
9034
 
9035
+ anthropic_resp = await _maybe_apply_recipe(anthropic_resp, body, openai_body, client)
8956
9036
  return anthropic_resp
8957
9037
 
8958
9038
  if PROXY_STREAM_HEARTBEAT_SECS > 0:
@@ -0,0 +1,244 @@
1
+ """Serving-layer recipe runtime (vLLM Semantic Router micro-agent patterns).
2
+
3
+ One bounded micro-agent loop behind one model API. Recipes:
4
+ - single : no collaboration (default).
5
+ - confidence : try the cheap primary; if a confidence signal is below
6
+ threshold, escalate the same single-answer turn to a stronger
7
+ backend (#1). The signal is pluggable: "heuristic" (cheap text)
8
+ or "selfverify" (a judge model rates the answer 0-10 -- the
9
+ gate-as-confidence idea at the serving layer, #5).
10
+ - fusion : run N breadth candidates from the primary, then a judge picks
11
+ the best; falls back to the primary on judge failure (#3).
12
+ A signal-driven selector (#2) chooses the recipe per request.
13
+
14
+ All orchestration is via INJECTED async callables (call_primary, call_judge) so
15
+ it is testable without a live backend. The proxy supplies real ones. Everything
16
+ is OFF by default and fails open.
17
+
18
+ Env:
19
+ PROXY_CONFIDENCE_ESCALATE=on master switch
20
+ PROXY_RECIPE=auto|single|confidence|fusion (default auto)
21
+ PROXY_CONFIDENCE_SIGNAL=heuristic|selfverify (default heuristic)
22
+ PROXY_CONFIDENCE_THRESHOLD=0.5
23
+ PROXY_FUSION_N=3 candidates incl. the primary (2..6)
24
+ PROXY_AUTO_FUSION_CHARS=600 auto: prompts longer than this -> fusion
25
+ PROXY_ESCALATE_MODEL / _ENDPOINT / _API_KEY stronger backend (judge/escalate)
26
+ """
27
+ from __future__ import annotations
28
+
29
+ import asyncio
30
+ import os
31
+ import re
32
+ from dataclasses import dataclass
33
+
34
+
35
+ @dataclass
36
+ class Settings:
37
+ enabled: bool
38
+ recipe: str
39
+ signal: str
40
+ threshold: float
41
+ fusion_n: int
42
+ auto_fusion_chars: int
43
+ model: str
44
+ endpoint: str
45
+ api_key: str
46
+
47
+ @classmethod
48
+ def from_env(cls) -> "Settings":
49
+ def flag(name, default="off"):
50
+ return os.environ.get(name, default).lower() not in {"", "0", "off", "false", "no"}
51
+
52
+ def num(name, default, cast=float):
53
+ try:
54
+ return cast(os.environ.get(name, str(default)))
55
+ except (ValueError, TypeError):
56
+ return default
57
+
58
+ recipe = os.environ.get("PROXY_RECIPE", "auto").lower()
59
+ if recipe not in {"auto", "single", "confidence", "fusion"}:
60
+ recipe = "auto"
61
+ signal = os.environ.get("PROXY_CONFIDENCE_SIGNAL", "heuristic").lower()
62
+ if signal not in {"heuristic", "selfverify"}:
63
+ signal = "heuristic"
64
+ return cls(
65
+ enabled=flag("PROXY_CONFIDENCE_ESCALATE"),
66
+ recipe=recipe,
67
+ signal=signal,
68
+ threshold=num("PROXY_CONFIDENCE_THRESHOLD", 0.5, float),
69
+ fusion_n=max(2, min(6, num("PROXY_FUSION_N", 3, int))),
70
+ auto_fusion_chars=num("PROXY_AUTO_FUSION_CHARS", 600, int),
71
+ model=os.environ.get("PROXY_ESCALATE_MODEL", ""),
72
+ endpoint=os.environ.get("PROXY_ESCALATE_ENDPOINT", ""),
73
+ api_key=os.environ.get("PROXY_ESCALATE_API_KEY", ""),
74
+ )
75
+
76
+ def backend_configured(self) -> bool:
77
+ return bool(self.model and self.endpoint)
78
+
79
+
80
+ # ---- helpers --------------------------------------------------------------
81
+ def latest_user_text(anthropic_body: dict) -> str:
82
+ for msg in reversed((anthropic_body or {}).get("messages", []) or []):
83
+ if msg.get("role") != "user":
84
+ continue
85
+ c = msg.get("content")
86
+ if isinstance(c, str):
87
+ return c
88
+ if isinstance(c, list):
89
+ return "".join(b.get("text", "") for b in c if isinstance(b, dict) and b.get("type") == "text")
90
+ return ""
91
+
92
+
93
+ def extract_text(anthropic_resp: dict) -> str:
94
+ parts = []
95
+ for blk in (anthropic_resp or {}).get("content", []) or []:
96
+ if isinstance(blk, dict) and blk.get("type") == "text":
97
+ parts.append(blk.get("text", ""))
98
+ return "".join(parts)
99
+
100
+
101
+ # ---- #2 recipe selection --------------------------------------------------
102
+ def select_recipe(anthropic_body: dict, settings: Settings, has_tools: bool) -> str:
103
+ if not settings.enabled or has_tools:
104
+ return "single"
105
+ if settings.recipe != "auto":
106
+ return settings.recipe
107
+ # Auto: signal-driven. Longer/harder prompts have higher reasoning variance
108
+ # -> fusion (breadth+judge); shorter -> confidence (cheap, escalate if weak).
109
+ if len(latest_user_text(anthropic_body)) >= settings.auto_fusion_chars and settings.backend_configured():
110
+ return "fusion"
111
+ return "confidence"
112
+
113
+
114
+ # ---- #1/#5 confidence signal ----------------------------------------------
115
+ _UNCERTAIN = re.compile(
116
+ r"\b(i'?m not sure|i am not sure|i don'?t know|i cannot|i can'?t (?:help|do|determine)|"
117
+ r"unable to|not certain|no idea|as an ai|i'?m sorry,? but)\b",
118
+ re.I,
119
+ )
120
+
121
+
122
+ def text_confidence(text: str) -> float:
123
+ t = (text or "").strip()
124
+ if not t:
125
+ return 0.0
126
+ if _UNCERTAIN.search(t):
127
+ return 0.2
128
+ if len(t) < 20:
129
+ return 0.3
130
+ return 0.9
131
+
132
+
133
+ def build_verify_payload(anthropic_body: dict, answer_text: str, settings: Settings) -> dict:
134
+ q = latest_user_text(anthropic_body)
135
+ prompt = (
136
+ "Rate from 0 to 10 how well this ANSWER satisfies the REQUEST "
137
+ "(0 = wrong or incomplete, 10 = fully correct and complete). "
138
+ "Reply with ONLY the number.\n\nREQUEST:\n" + q + "\n\nANSWER:\n" + answer_text
139
+ )
140
+ return {"model": settings.model, "max_tokens": 16,
141
+ "messages": [{"role": "user", "content": prompt}]}
142
+
143
+
144
+ def parse_verify_score(text: str):
145
+ m = re.search(r"\d+(?:\.\d+)?", text or "")
146
+ if not m:
147
+ return None
148
+ return max(0.0, min(1.0, float(m.group(0)) / 10.0))
149
+
150
+
151
+ # ---- escalation / fusion payloads -----------------------------------------
152
+ def build_escalation_payload(anthropic_body: dict, settings: Settings) -> dict:
153
+ payload = {"model": settings.model,
154
+ "max_tokens": anthropic_body.get("max_tokens", 4096),
155
+ "messages": anthropic_body.get("messages", [])}
156
+ if anthropic_body.get("system"):
157
+ payload["system"] = anthropic_body["system"]
158
+ return payload
159
+
160
+
161
+ def build_fusion_variants(openai_body: dict, n: int) -> list[dict]:
162
+ """N-1 extra primary variants (the live response is candidate 0). Vary
163
+ temperature for breadth; everything else identical."""
164
+ out = []
165
+ for i in range(max(0, n - 1)):
166
+ v = dict(openai_body)
167
+ v["stream"] = False
168
+ v["temperature"] = round(0.4 + 0.2 * i, 2)
169
+ out.append(v)
170
+ return out
171
+
172
+
173
+ def build_judge_payload(anthropic_body: dict, candidate_texts: list[str], settings: Settings) -> dict:
174
+ q = latest_user_text(anthropic_body)
175
+ listing = "\n\n".join(f"[{i}]\n{t}" for i, t in enumerate(candidate_texts))
176
+ prompt = (
177
+ "You are a strict judge. Choose the SINGLE best answer to the REQUEST. "
178
+ "Reply with ONLY the index number of the best answer.\n\nREQUEST:\n"
179
+ + q + "\n\nANSWERS:\n" + listing
180
+ )
181
+ return {"model": settings.model, "max_tokens": 8,
182
+ "messages": [{"role": "user", "content": prompt}]}
183
+
184
+
185
+ def parse_judge_index(text: str, n: int):
186
+ m = re.search(r"\d+", text or "")
187
+ if not m:
188
+ return None
189
+ i = int(m.group(0))
190
+ return i if 0 <= i < n else None
191
+
192
+
193
+ def should_escalate(text: str, settings: Settings, has_tools: bool) -> bool:
194
+ """Back-compat (heuristic confidence path)."""
195
+ if not settings.enabled or not settings.backend_configured() or has_tools:
196
+ return False
197
+ return text_confidence(text) < settings.threshold
198
+
199
+
200
+ # ---- orchestration (injected callables) -----------------------------------
201
+ async def _confidence_score(text, anthropic_body, settings, call_judge):
202
+ if settings.signal == "selfverify" and settings.backend_configured() and call_judge is not None:
203
+ jr = await call_judge(build_verify_payload(anthropic_body, text, settings))
204
+ score = parse_verify_score(extract_text(jr)) if isinstance(jr, dict) else None
205
+ if score is not None:
206
+ return score
207
+ return text_confidence(text)
208
+
209
+
210
+ async def apply_recipe(primary_resp, anthropic_body, openai_body, settings, has_tools,
211
+ call_primary, call_judge):
212
+ """Dispatch to the selected recipe. Returns an anthropic response dict.
213
+ call_primary(openai_variant)->anthropic_resp|None ; call_judge(anthropic_payload)->anthropic_resp|None.
214
+ Fails open: returns primary_resp on any problem."""
215
+ try:
216
+ recipe = select_recipe(anthropic_body, settings, has_tools)
217
+ if recipe == "single" or not isinstance(primary_resp, dict):
218
+ return primary_resp
219
+ primary_text = extract_text(primary_resp)
220
+
221
+ if recipe == "confidence":
222
+ conf = await _confidence_score(primary_text, anthropic_body, settings, call_judge)
223
+ if conf < settings.threshold and settings.backend_configured() and call_judge is not None:
224
+ esc = await call_judge(build_escalation_payload(anthropic_body, settings))
225
+ if isinstance(esc, dict):
226
+ return esc
227
+ return primary_resp
228
+
229
+ if recipe == "fusion":
230
+ variants = build_fusion_variants(openai_body or {}, settings.fusion_n)
231
+ results = await asyncio.gather(*[call_primary(v) for v in variants],
232
+ return_exceptions=True)
233
+ candidates = [primary_resp] + [r for r in results if isinstance(r, dict)]
234
+ if len(candidates) <= 1 or not settings.backend_configured() or call_judge is None:
235
+ return primary_resp
236
+ texts = [extract_text(c) for c in candidates]
237
+ jr = await call_judge(build_judge_payload(anthropic_body, texts, settings))
238
+ idx = parse_judge_index(extract_text(jr), len(candidates)) if isinstance(jr, dict) else None
239
+ if idx is not None:
240
+ return candidates[idx]
241
+ return primary_resp # fallback to best valid evidence
242
+ except Exception:
243
+ return primary_resp
244
+ return primary_resp
@@ -0,0 +1,109 @@
1
+ """Tests for the serving-layer recipe runtime (Confidence #1/#5, Fusion #3, selector #2)."""
2
+ import asyncio
3
+ import importlib.util
4
+ import sys
5
+ import unittest
6
+ from pathlib import Path
7
+
8
+ mod_path = Path(__file__).resolve().parents[3] / "tools" / "agents" / "scripts" / "confidence_escalation.py"
9
+ spec = importlib.util.spec_from_file_location("confidence_escalation", mod_path)
10
+ ce = importlib.util.module_from_spec(spec)
11
+ sys.modules["confidence_escalation"] = ce
12
+ spec.loader.exec_module(ce)
13
+
14
+
15
+ def S(enabled=True, recipe="auto", signal="heuristic", threshold=0.5, fusion_n=3,
16
+ auto_chars=600, model="opus", endpoint="http://x/", key="k"):
17
+ return ce.Settings(enabled=enabled, recipe=recipe, signal=signal, threshold=threshold,
18
+ fusion_n=fusion_n, auto_fusion_chars=auto_chars, model=model,
19
+ endpoint=endpoint, api_key=key)
20
+
21
+
22
+ def resp(text):
23
+ return {"content": [{"type": "text", "text": text}]}
24
+
25
+
26
+ def body(user="hi"):
27
+ return {"model": "qwen", "max_tokens": 100, "messages": [{"role": "user", "content": user}]}
28
+
29
+
30
+ def run(coro):
31
+ return asyncio.new_event_loop().run_until_complete(coro)
32
+
33
+
34
+ class HelpersTest(unittest.TestCase):
35
+ def test_text_confidence(self):
36
+ self.assertEqual(ce.text_confidence(""), 0.0)
37
+ self.assertEqual(ce.text_confidence("I don't know"), 0.2)
38
+ self.assertGreater(ce.text_confidence("A thorough complete answer to the question."), 0.5)
39
+
40
+ def test_parse_verify_score(self):
41
+ self.assertAlmostEqual(ce.parse_verify_score("8"), 0.8)
42
+ self.assertEqual(ce.parse_verify_score("score: 3 of 10"), 0.3)
43
+ self.assertIsNone(ce.parse_verify_score("n/a"))
44
+
45
+ def test_parse_judge_index(self):
46
+ self.assertEqual(ce.parse_judge_index("The best is [2]", 3), 2)
47
+ self.assertIsNone(ce.parse_judge_index("9", 3))
48
+
49
+ def test_fusion_variants(self):
50
+ v = ce.build_fusion_variants({"model": "qwen", "messages": []}, 3)
51
+ self.assertEqual(len(v), 2) # n-1 (primary is candidate 0)
52
+ self.assertEqual([x["temperature"] for x in v], [0.4, 0.6])
53
+
54
+
55
+ class SelectorTest(unittest.TestCase): # #2
56
+ def test_disabled_or_tools_single(self):
57
+ self.assertEqual(ce.select_recipe(body(), S(enabled=False), False), "single")
58
+ self.assertEqual(ce.select_recipe(body(), S(), True), "single")
59
+
60
+ def test_explicit_recipe(self):
61
+ self.assertEqual(ce.select_recipe(body(), S(recipe="fusion"), False), "fusion")
62
+
63
+ def test_auto_short_confidence_long_fusion(self):
64
+ self.assertEqual(ce.select_recipe(body("hi"), S(recipe="auto"), False), "confidence")
65
+ self.assertEqual(ce.select_recipe(body("x" * 700), S(recipe="auto"), False), "fusion")
66
+
67
+
68
+ class ApplyConfidenceTest(unittest.TestCase): # #1 / #5
69
+ def test_low_confidence_escalates(self):
70
+ async def judge(p): return resp("ESCALATED ANSWER")
71
+ out = run(ce.apply_recipe(resp("I cannot help"), body(), {}, S(recipe="confidence"),
72
+ False, None, judge))
73
+ self.assertEqual(ce.extract_text(out), "ESCALATED ANSWER")
74
+
75
+ def test_high_confidence_keeps_primary(self):
76
+ async def judge(p): raise AssertionError("should not be called")
77
+ out = run(ce.apply_recipe(resp("A complete confident answer here."), body(), {},
78
+ S(recipe="confidence"), False, None, judge))
79
+ self.assertEqual(ce.extract_text(out), "A complete confident answer here.")
80
+
81
+ def test_selfverify_uses_judge_score(self): # #5
82
+ calls = {"n": 0}
83
+ async def judge(p):
84
+ calls["n"] += 1
85
+ return resp("9") # high score -> no escalation
86
+ out = run(ce.apply_recipe(resp("short"), body(), {}, S(recipe="confidence", signal="selfverify"),
87
+ False, None, judge))
88
+ self.assertEqual(calls["n"], 1) # verify call happened
89
+ self.assertEqual(ce.extract_text(out), "short") # high score, kept primary
90
+
91
+
92
+ class ApplyFusionTest(unittest.TestCase): # #3
93
+ def test_fusion_fans_out_and_judge_picks(self):
94
+ async def primary(v): return resp("candidate-1")
95
+ async def judge(p): return resp("1") # pick index 1
96
+ out = run(ce.apply_recipe(resp("candidate-0"), body(), {"model": "qwen", "messages": []},
97
+ S(recipe="fusion", fusion_n=2), False, primary, judge))
98
+ self.assertEqual(ce.extract_text(out), "candidate-1")
99
+
100
+ def test_fusion_judge_failure_falls_back_to_primary(self):
101
+ async def primary(v): return resp("c1")
102
+ async def judge(p): return None # judge failed
103
+ out = run(ce.apply_recipe(resp("c0"), body(), {"model": "qwen", "messages": []},
104
+ S(recipe="fusion", fusion_n=2), False, primary, judge))
105
+ self.assertEqual(ce.extract_text(out), "c0")
106
+
107
+
108
+ if __name__ == "__main__":
109
+ unittest.main()