@miller-tech/uap 1.85.0 → 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 +55 -25
- package/tools/agents/scripts/confidence_escalation.py +190 -50
- package/tools/agents/tests/test_confidence_escalation.py +83 -30
package/package.json
CHANGED
|
Binary file
|
|
Binary file
|
|
@@ -8003,38 +8003,68 @@ def openai_to_anthropic_response(
|
|
|
8003
8003
|
}
|
|
8004
8004
|
|
|
8005
8005
|
|
|
8006
|
-
async def
|
|
8007
|
-
"""
|
|
8008
|
-
|
|
8009
|
-
|
|
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
8010
|
if _ce is None or not isinstance(anthropic_resp, dict):
|
|
8011
8011
|
return anthropic_resp
|
|
8012
8012
|
try:
|
|
8013
8013
|
settings = _ce.Settings.from_env()
|
|
8014
|
-
|
|
8015
|
-
if not _ce.should_escalate(text, settings, _has_tool_definitions(anthropic_body)):
|
|
8014
|
+
if not settings.enabled:
|
|
8016
8015
|
return anthropic_resp
|
|
8017
|
-
|
|
8018
|
-
|
|
8019
|
-
|
|
8020
|
-
|
|
8021
|
-
|
|
8022
|
-
|
|
8023
|
-
|
|
8024
|
-
|
|
8025
|
-
|
|
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:
|
|
8026
8059
|
logger.warning(
|
|
8027
|
-
"
|
|
8028
|
-
|
|
8060
|
+
"RECIPE applied: recipe=%s signal=%s -> response changed",
|
|
8061
|
+
_ce.select_recipe(anthropic_body, settings, _has_tool_definitions(anthropic_body)),
|
|
8062
|
+
settings.signal,
|
|
8029
8063
|
)
|
|
8030
|
-
|
|
8031
|
-
logger.warning(
|
|
8032
|
-
"CONFIDENCE ESCALATION: backend %s returned %d; keeping primary",
|
|
8033
|
-
settings.model, resp.status_code,
|
|
8034
|
-
)
|
|
8064
|
+
return result if isinstance(result, dict) else anthropic_resp
|
|
8035
8065
|
except Exception as exc:
|
|
8036
|
-
logger.warning("
|
|
8037
|
-
|
|
8066
|
+
logger.warning("RECIPE: failed (%s); keeping primary answer", exc)
|
|
8067
|
+
return anthropic_resp
|
|
8038
8068
|
|
|
8039
8069
|
|
|
8040
8070
|
async def _heartbeat_then_buffered(produce_coro, model: str):
|
|
@@ -9002,7 +9032,7 @@ async def messages(request: Request):
|
|
|
9002
9032
|
"REQUIRED TOOL STREAM GUARDRAIL: served stream response via guarded non-stream path"
|
|
9003
9033
|
)
|
|
9004
9034
|
|
|
9005
|
-
anthropic_resp = await
|
|
9035
|
+
anthropic_resp = await _maybe_apply_recipe(anthropic_resp, body, openai_body, client)
|
|
9006
9036
|
return anthropic_resp
|
|
9007
9037
|
|
|
9008
9038
|
if PROXY_STREAM_HEARTBEAT_SECS > 0:
|
|
@@ -1,26 +1,32 @@
|
|
|
1
|
-
"""
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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)
|
|
21
26
|
"""
|
|
22
27
|
from __future__ import annotations
|
|
23
28
|
|
|
29
|
+
import asyncio
|
|
24
30
|
import os
|
|
25
31
|
import re
|
|
26
32
|
from dataclasses import dataclass
|
|
@@ -29,23 +35,39 @@ from dataclasses import dataclass
|
|
|
29
35
|
@dataclass
|
|
30
36
|
class Settings:
|
|
31
37
|
enabled: bool
|
|
38
|
+
recipe: str
|
|
39
|
+
signal: str
|
|
32
40
|
threshold: float
|
|
41
|
+
fusion_n: int
|
|
42
|
+
auto_fusion_chars: int
|
|
33
43
|
model: str
|
|
34
44
|
endpoint: str
|
|
35
45
|
api_key: str
|
|
36
46
|
|
|
37
47
|
@classmethod
|
|
38
48
|
def from_env(cls) -> "Settings":
|
|
39
|
-
|
|
40
|
-
"", "0", "off", "false", "no"
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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"
|
|
46
64
|
return cls(
|
|
47
|
-
enabled=
|
|
48
|
-
|
|
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),
|
|
49
71
|
model=os.environ.get("PROXY_ESCALATE_MODEL", ""),
|
|
50
72
|
endpoint=os.environ.get("PROXY_ESCALATE_ENDPOINT", ""),
|
|
51
73
|
api_key=os.environ.get("PROXY_ESCALATE_API_KEY", ""),
|
|
@@ -55,6 +77,41 @@ class Settings:
|
|
|
55
77
|
return bool(self.model and self.endpoint)
|
|
56
78
|
|
|
57
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 ----------------------------------------------
|
|
58
115
|
_UNCERTAIN = re.compile(
|
|
59
116
|
r"\b(i'?m not sure|i am not sure|i don'?t know|i cannot|i can'?t (?:help|do|determine)|"
|
|
60
117
|
r"unable to|not certain|no idea|as an ai|i'?m sorry,? but)\b",
|
|
@@ -63,8 +120,6 @@ _UNCERTAIN = re.compile(
|
|
|
63
120
|
|
|
64
121
|
|
|
65
122
|
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
123
|
t = (text or "").strip()
|
|
69
124
|
if not t:
|
|
70
125
|
return 0.0
|
|
@@ -75,30 +130,115 @@ def text_confidence(text: str) -> float:
|
|
|
75
130
|
return 0.9
|
|
76
131
|
|
|
77
132
|
|
|
78
|
-
def
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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}]}
|
|
85
142
|
|
|
86
143
|
|
|
87
|
-
def
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
return text_confidence(text) < settings.threshold
|
|
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))
|
|
93
149
|
|
|
94
150
|
|
|
151
|
+
# ---- escalation / fusion payloads -----------------------------------------
|
|
95
152
|
def build_escalation_payload(anthropic_body: dict, settings: Settings) -> dict:
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
"max_tokens": anthropic_body.get("max_tokens", 4096),
|
|
100
|
-
"messages": anthropic_body.get("messages", []),
|
|
101
|
-
}
|
|
153
|
+
payload = {"model": settings.model,
|
|
154
|
+
"max_tokens": anthropic_body.get("max_tokens", 4096),
|
|
155
|
+
"messages": anthropic_body.get("messages", [])}
|
|
102
156
|
if anthropic_body.get("system"):
|
|
103
157
|
payload["system"] = anthropic_body["system"]
|
|
104
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
|
|
@@ -1,55 +1,108 @@
|
|
|
1
|
-
"""Tests for the
|
|
1
|
+
"""Tests for the serving-layer recipe runtime (Confidence #1/#5, Fusion #3, selector #2)."""
|
|
2
|
+
import asyncio
|
|
2
3
|
import importlib.util
|
|
4
|
+
import sys
|
|
3
5
|
import unittest
|
|
4
6
|
from pathlib import Path
|
|
5
7
|
|
|
6
8
|
mod_path = Path(__file__).resolve().parents[3] / "tools" / "agents" / "scripts" / "confidence_escalation.py"
|
|
7
9
|
spec = importlib.util.spec_from_file_location("confidence_escalation", mod_path)
|
|
8
10
|
ce = importlib.util.module_from_spec(spec)
|
|
9
|
-
|
|
11
|
+
sys.modules["confidence_escalation"] = ce
|
|
12
|
+
spec.loader.exec_module(ce)
|
|
10
13
|
|
|
11
14
|
|
|
12
|
-
def S(enabled=True,
|
|
13
|
-
|
|
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)
|
|
14
20
|
|
|
15
21
|
|
|
16
|
-
|
|
17
|
-
|
|
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):
|
|
18
36
|
self.assertEqual(ce.text_confidence(""), 0.0)
|
|
19
37
|
self.assertEqual(ce.text_confidence("I don't know"), 0.2)
|
|
20
|
-
self.
|
|
21
|
-
self.assertGreater(ce.text_confidence("Here is a complete, detailed answer to your question."), 0.5)
|
|
38
|
+
self.assertGreater(ce.text_confidence("A thorough complete answer to the question."), 0.5)
|
|
22
39
|
|
|
23
|
-
def
|
|
24
|
-
|
|
25
|
-
self.assertEqual(ce.
|
|
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"))
|
|
26
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))
|
|
27
48
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
self.
|
|
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])
|
|
31
53
|
|
|
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
54
|
|
|
35
|
-
|
|
36
|
-
|
|
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")
|
|
37
59
|
|
|
38
|
-
def
|
|
39
|
-
self.
|
|
60
|
+
def test_explicit_recipe(self):
|
|
61
|
+
self.assertEqual(ce.select_recipe(body(), S(recipe="fusion"), False), "fusion")
|
|
40
62
|
|
|
41
|
-
def
|
|
42
|
-
self.
|
|
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")
|
|
43
66
|
|
|
44
67
|
|
|
45
|
-
class
|
|
46
|
-
def
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
self.assertEqual(
|
|
51
|
-
|
|
52
|
-
|
|
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")
|
|
53
106
|
|
|
54
107
|
|
|
55
108
|
if __name__ == "__main__":
|