@miller-tech/uap 1.88.0 → 1.89.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/confidence_escalation.py +59 -1
- package/tools/agents/tests/test_confidence_escalation.py +54 -3
package/package.json
CHANGED
|
Binary file
|
|
Binary file
|
|
@@ -42,6 +42,7 @@ class Settings:
|
|
|
42
42
|
signal: str
|
|
43
43
|
threshold: float
|
|
44
44
|
fusion_n: int
|
|
45
|
+
remom_quorum: int
|
|
45
46
|
auto_fusion_chars: int
|
|
46
47
|
model: str
|
|
47
48
|
endpoint: str
|
|
@@ -59,7 +60,7 @@ class Settings:
|
|
|
59
60
|
return default
|
|
60
61
|
|
|
61
62
|
recipe = os.environ.get("PROXY_RECIPE", "auto").lower()
|
|
62
|
-
if recipe not in {"auto", "single", "confidence", "fusion"}:
|
|
63
|
+
if recipe not in {"auto", "single", "confidence", "fusion", "ratings", "remom", "workflow"}:
|
|
63
64
|
recipe = "auto"
|
|
64
65
|
signal = os.environ.get("PROXY_CONFIDENCE_SIGNAL", "heuristic").lower()
|
|
65
66
|
if signal not in {"heuristic", "selfverify"}:
|
|
@@ -70,6 +71,7 @@ class Settings:
|
|
|
70
71
|
signal=signal,
|
|
71
72
|
threshold=num("PROXY_CONFIDENCE_THRESHOLD", 0.5, float),
|
|
72
73
|
fusion_n=max(2, min(6, num("PROXY_FUSION_N", 3, int))),
|
|
74
|
+
remom_quorum=max(1, min(6, num("PROXY_REMOM_QUORUM", 2, int))),
|
|
73
75
|
auto_fusion_chars=num("PROXY_AUTO_FUSION_CHARS", 600, int),
|
|
74
76
|
model=os.environ.get("PROXY_ESCALATE_MODEL", ""),
|
|
75
77
|
endpoint=os.environ.get("PROXY_ESCALATE_ENDPOINT", ""),
|
|
@@ -321,6 +323,26 @@ def parse_judge_index(text: str, n: int):
|
|
|
321
323
|
return i if 0 <= i < n else None
|
|
322
324
|
|
|
323
325
|
|
|
326
|
+
def build_synthesis_payload(anthropic_body: dict, candidate_texts: list[str], settings: Settings) -> dict:
|
|
327
|
+
q = latest_user_text(anthropic_body)
|
|
328
|
+
listing = "\n\n".join(f"[{i}]\n{t}" for i, t in enumerate(candidate_texts))
|
|
329
|
+
prompt = (
|
|
330
|
+
"Synthesize a SINGLE best answer to the REQUEST by merging the correct, "
|
|
331
|
+
"complementary parts of the candidate answers below. Resolve disagreements "
|
|
332
|
+
"and keep the required output format. Reply with ONLY the final answer."
|
|
333
|
+
"\n\nREQUEST:\n" + q + "\n\nCANDIDATES:\n" + listing
|
|
334
|
+
)
|
|
335
|
+
return {"model": settings.model, "max_tokens": anthropic_body.get("max_tokens", 4096),
|
|
336
|
+
"messages": [{"role": "user", "content": prompt}]}
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
async def _fanout_candidates(primary_resp, openai_body, settings, call_primary):
|
|
340
|
+
"""[primary] + up to fusion_n-1 temperature-varied breadth candidates."""
|
|
341
|
+
variants = build_fusion_variants(openai_body or {}, settings.fusion_n)
|
|
342
|
+
results = await asyncio.gather(*[call_primary(v) for v in variants], return_exceptions=True)
|
|
343
|
+
return [primary_resp] + [r for r in results if isinstance(r, dict)]
|
|
344
|
+
|
|
345
|
+
|
|
324
346
|
def should_escalate(text: str, settings: Settings, has_tools: bool) -> bool:
|
|
325
347
|
"""Back-compat (heuristic confidence path)."""
|
|
326
348
|
if not settings.enabled or not settings.backend_configured() or has_tools:
|
|
@@ -370,6 +392,42 @@ async def apply_recipe(primary_resp, anthropic_body, openai_body, settings, has_
|
|
|
370
392
|
if idx is not None:
|
|
371
393
|
return candidates[idx]
|
|
372
394
|
return primary_resp # fallback to best valid evidence
|
|
395
|
+
|
|
396
|
+
if recipe == "ratings":
|
|
397
|
+
# Bounded ensemble: rate each candidate independently, pick the best.
|
|
398
|
+
candidates = await _fanout_candidates(primary_resp, openai_body, settings, call_primary)
|
|
399
|
+
if len(candidates) <= 1 or not settings.backend_configured() or call_judge is None:
|
|
400
|
+
return primary_resp
|
|
401
|
+
rs = await asyncio.gather(
|
|
402
|
+
*[call_judge(build_verify_payload(anthropic_body, extract_text(c), settings))
|
|
403
|
+
for c in candidates],
|
|
404
|
+
return_exceptions=True,
|
|
405
|
+
)
|
|
406
|
+
scored = []
|
|
407
|
+
for c, jr2 in zip(candidates, rs):
|
|
408
|
+
sc = parse_verify_score(extract_text(jr2)) if isinstance(jr2, dict) else None
|
|
409
|
+
scored.append((sc if sc is not None else text_confidence(extract_text(c)), c))
|
|
410
|
+
return max(scored, key=lambda x: x[0])[1]
|
|
411
|
+
|
|
412
|
+
if recipe == "remom":
|
|
413
|
+
# Breadth -> quorum -> synthesis into the output contract; fall back
|
|
414
|
+
# to the best valid evidence if synthesis fails (no API error).
|
|
415
|
+
candidates = await _fanout_candidates(primary_resp, openai_body, settings, call_primary)
|
|
416
|
+
valid = [c for c in candidates if extract_text(c).strip()]
|
|
417
|
+
if len(valid) >= settings.remom_quorum and settings.backend_configured() and call_judge is not None:
|
|
418
|
+
synth = await call_judge(
|
|
419
|
+
build_synthesis_payload(anthropic_body, [extract_text(c) for c in valid], settings)
|
|
420
|
+
)
|
|
421
|
+
if isinstance(synth, dict) and extract_text(synth).strip():
|
|
422
|
+
return synth
|
|
423
|
+
return max(valid, key=lambda c: len(extract_text(c))) if valid else primary_resp
|
|
424
|
+
|
|
425
|
+
if recipe == "workflow":
|
|
426
|
+
# Workflows (planner/patcher/verifier under a contract) are the
|
|
427
|
+
# deliver convergence loop's job — it owns the real execution +
|
|
428
|
+
# acceptance gates and repo state a stateless serving turn lacks.
|
|
429
|
+
# Pass through; the harness routes workflow tasks through uap deliver.
|
|
430
|
+
return primary_resp
|
|
373
431
|
except Exception:
|
|
374
432
|
return primary_resp
|
|
375
433
|
return primary_resp
|
|
@@ -15,10 +15,10 @@ spec.loader.exec_module(ce)
|
|
|
15
15
|
|
|
16
16
|
|
|
17
17
|
def S(enabled=True, recipe="auto", signal="heuristic", threshold=0.5, fusion_n=3,
|
|
18
|
-
auto_chars=600, model="opus", endpoint="http://x/", key="k"):
|
|
18
|
+
auto_chars=600, model="opus", endpoint="http://x/", key="k", remom_quorum=2):
|
|
19
19
|
return ce.Settings(enabled=enabled, recipe=recipe, signal=signal, threshold=threshold,
|
|
20
|
-
fusion_n=fusion_n,
|
|
21
|
-
endpoint=endpoint, api_key=key)
|
|
20
|
+
fusion_n=fusion_n, remom_quorum=remom_quorum, auto_fusion_chars=auto_chars,
|
|
21
|
+
model=model, endpoint=endpoint, api_key=key)
|
|
22
22
|
|
|
23
23
|
|
|
24
24
|
def resp(text):
|
|
@@ -180,5 +180,56 @@ class CrossProcessSignalTest(unittest.TestCase): # reactor -> proxy
|
|
|
180
180
|
self.assertEqual(r, "fusion") # reactor signal overrides the simple self-classification
|
|
181
181
|
|
|
182
182
|
|
|
183
|
+
|
|
184
|
+
class ApplyRatingsTest(unittest.TestCase): # Ratings
|
|
185
|
+
def test_picks_highest_rated_candidate(self):
|
|
186
|
+
async def primary(v): return resp("cand-1")
|
|
187
|
+
scores = iter(["3", "9"]) # primary rated 3, fanout rated 9
|
|
188
|
+
async def judge(p): return resp(next(scores))
|
|
189
|
+
out = run(ce.apply_recipe(resp("cand-0"), body(), {"model": "q", "messages": []},
|
|
190
|
+
S(recipe="ratings", fusion_n=2), False, primary, judge))
|
|
191
|
+
self.assertEqual(ce.extract_text(out), "cand-1")
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
class ApplyReMoMTest(unittest.TestCase): # ReMoM
|
|
195
|
+
def test_synthesizes_when_quorum_met(self):
|
|
196
|
+
async def primary(v): return resp("evidence B")
|
|
197
|
+
async def judge(p): return resp("MERGED ANSWER")
|
|
198
|
+
out = run(ce.apply_recipe(resp("evidence A"), body(), {"model": "q", "messages": []},
|
|
199
|
+
S(recipe="remom", fusion_n=2), False, primary, judge))
|
|
200
|
+
self.assertEqual(ce.extract_text(out), "MERGED ANSWER")
|
|
201
|
+
|
|
202
|
+
def test_falls_back_to_best_evidence_when_synthesis_fails(self):
|
|
203
|
+
async def primary(v): return resp("a longer, more complete candidate answer")
|
|
204
|
+
async def judge(p): return None # synthesis failed
|
|
205
|
+
out = run(ce.apply_recipe(resp("short"), body(), {"model": "q", "messages": []},
|
|
206
|
+
S(recipe="remom", fusion_n=2), False, primary, judge))
|
|
207
|
+
self.assertEqual(ce.extract_text(out), "a longer, more complete candidate answer")
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
class WorkflowTest(unittest.TestCase):
|
|
211
|
+
def test_workflow_passes_through(self): # Workflows = deliver's job
|
|
212
|
+
async def primary(v): return resp("x")
|
|
213
|
+
async def judge(p): return resp("y")
|
|
214
|
+
out = run(ce.apply_recipe(resp("PRIMARY"), body(), {}, S(recipe="workflow"), False, primary, judge))
|
|
215
|
+
self.assertEqual(ce.extract_text(out), "PRIMARY")
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
class SettingsExtTest(unittest.TestCase):
|
|
219
|
+
def test_remom_quorum_default(self):
|
|
220
|
+
import os, tempfile
|
|
221
|
+
for k in ("PROXY_REMOM_QUORUM",): os.environ.pop(k, None)
|
|
222
|
+
self.assertEqual(ce.Settings.from_env().remom_quorum, 2)
|
|
223
|
+
|
|
224
|
+
def test_new_recipes_valid(self):
|
|
225
|
+
import os
|
|
226
|
+
for r in ("ratings", "remom", "workflow"):
|
|
227
|
+
os.environ["PROXY_RECIPE"] = r
|
|
228
|
+
try:
|
|
229
|
+
self.assertEqual(ce.Settings.from_env().recipe, r)
|
|
230
|
+
finally:
|
|
231
|
+
os.environ.pop("PROXY_RECIPE", None)
|
|
232
|
+
|
|
233
|
+
|
|
183
234
|
if __name__ == "__main__":
|
|
184
235
|
unittest.main()
|