@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 +1 -1
- package/src/policies/enforcers/__pycache__/_common.cpython-312.pyc +0 -0
- package/tools/agents/scripts/__pycache__/toolcall_path_normalizer.cpython-312.pyc +0 -0
- package/tools/agents/scripts/anthropic_proxy.py +80 -0
- package/tools/agents/scripts/confidence_escalation.py +244 -0
- package/tools/agents/tests/test_confidence_escalation.py +109 -0
package/package.json
CHANGED
|
Binary file
|
|
Binary file
|
|
@@ -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()
|