Spaces:
Sleeping
Sleeping
yc1838 commited on
Commit ·
bad5fbe
1
Parent(s): 335df66
update supervisor nudging logic and fix tests
Browse files- src/lilith_agent/app.py +10 -4
- tests/test_graph.py +14 -13
src/lilith_agent/app.py
CHANGED
|
@@ -80,6 +80,7 @@ _FAIL_SAFE_RECURSION_HEADROOM = 4
|
|
| 80 |
_SUPERVISOR_MIN_TOOL_CALLS = 5
|
| 81 |
_SUPERVISOR_RECENT_MESSAGES = 12
|
| 82 |
_SUPERVISOR_REVIEW_MAX = 3
|
|
|
|
| 83 |
|
| 84 |
|
| 85 |
_RESPONSE_METADATA_NOISE_KEYS = frozenset({
|
|
@@ -828,6 +829,12 @@ def build_react_agent(cfg: Config):
|
|
| 828 |
return {"messages": [response]}
|
| 829 |
|
| 830 |
def supervisor_node(state):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 831 |
tool_calls_this_turn = _count_tool_calls_since_last_human(state["messages"])
|
| 832 |
if supervisor_model is None or tool_calls_this_turn < _SUPERVISOR_MIN_TOOL_CALLS:
|
| 833 |
return {
|
|
@@ -847,8 +854,9 @@ def build_react_agent(cfg: Config):
|
|
| 847 |
"Return ONLY JSON with keys status, best_answer, guidance. "
|
| 848 |
"status must be continue, nudge, or finalize. Use nudge when evidence likely supports "
|
| 849 |
"an answer but one more agent turn is acceptable. Use finalize only when a concrete "
|
| 850 |
-
"submit-ready answer is available or the evidence is conclusive.
|
| 851 |
-
"
|
|
|
|
| 852 |
"sure in best_answer. If no concrete submit-ready answer is available, set best_answer "
|
| 853 |
"to an empty string and use guidance to force the agent to make its best guess."
|
| 854 |
)
|
|
@@ -863,8 +871,6 @@ def build_react_agent(cfg: Config):
|
|
| 863 |
if _is_placeholder_answer(best_answer):
|
| 864 |
print(f"[supervisor] discarded placeholder best_answer={best_answer[:80]!r}", flush=True)
|
| 865 |
best_answer = ""
|
| 866 |
-
if status == "finalize" and state.get("supervisor_nudges", 0) > 0 and not best_answer:
|
| 867 |
-
status = "nudge"
|
| 868 |
log.info(
|
| 869 |
"[supervisor] status=%s best=%r guidance=%r",
|
| 870 |
status,
|
|
|
|
| 80 |
_SUPERVISOR_MIN_TOOL_CALLS = 5
|
| 81 |
_SUPERVISOR_RECENT_MESSAGES = 12
|
| 82 |
_SUPERVISOR_REVIEW_MAX = 3
|
| 83 |
+
_SUPERVISOR_MAX_NUDGES = 5
|
| 84 |
|
| 85 |
|
| 86 |
_RESPONSE_METADATA_NOISE_KEYS = frozenset({
|
|
|
|
| 829 |
return {"messages": [response]}
|
| 830 |
|
| 831 |
def supervisor_node(state):
|
| 832 |
+
if state.get("supervisor_nudges", 0) >= _SUPERVISOR_MAX_NUDGES:
|
| 833 |
+
return {
|
| 834 |
+
"supervisor_decision": "finalize",
|
| 835 |
+
"supervisor_best_answer": state.get("supervisor_best_answer", ""),
|
| 836 |
+
"supervisor_guidance": "Nudge cap reached. Commit to best available answer.",
|
| 837 |
+
}
|
| 838 |
tool_calls_this_turn = _count_tool_calls_since_last_human(state["messages"])
|
| 839 |
if supervisor_model is None or tool_calls_this_turn < _SUPERVISOR_MIN_TOOL_CALLS:
|
| 840 |
return {
|
|
|
|
| 854 |
"Return ONLY JSON with keys status, best_answer, guidance. "
|
| 855 |
"status must be continue, nudge, or finalize. Use nudge when evidence likely supports "
|
| 856 |
"an answer but one more agent turn is acceptable. Use finalize only when a concrete "
|
| 857 |
+
"submit-ready answer is available or the evidence is conclusive. If you have already nudged "
|
| 858 |
+
"five times and the agent has not improved, use finalize to force termination — do not nudge "
|
| 859 |
+
"repeatedly. Never put placeholder values like unknown, n/a, none, or not "
|
| 860 |
"sure in best_answer. If no concrete submit-ready answer is available, set best_answer "
|
| 861 |
"to an empty string and use guidance to force the agent to make its best guess."
|
| 862 |
)
|
|
|
|
| 871 |
if _is_placeholder_answer(best_answer):
|
| 872 |
print(f"[supervisor] discarded placeholder best_answer={best_answer[:80]!r}", flush=True)
|
| 873 |
best_answer = ""
|
|
|
|
|
|
|
| 874 |
log.info(
|
| 875 |
"[supervisor] status=%s best=%r guidance=%r",
|
| 876 |
status,
|
tests/test_graph.py
CHANGED
|
@@ -428,15 +428,13 @@ def test_supervisor_finalizer_rejects_unknown_best_answer_and_forces_best_guess(
|
|
| 428 |
assert strong.finalizer_calls == 1
|
| 429 |
|
| 430 |
|
| 431 |
-
def
|
| 432 |
class FakeBoundModel:
|
| 433 |
def __init__(self):
|
| 434 |
self.calls = 0
|
| 435 |
|
| 436 |
def invoke(self, messages):
|
| 437 |
self.calls += 1
|
| 438 |
-
if self.calls >= 3 and any("SUPERVISOR" in str(getattr(m, "content", "")) for m in messages):
|
| 439 |
-
return AIMessage(content="Final Answer: recovered guess")
|
| 440 |
return _ai_with_calls([
|
| 441 |
{
|
| 442 |
"id": f"call-{self.calls}",
|
|
@@ -458,10 +456,11 @@ def test_supervisor_does_not_finalize_placeholder_after_prior_nudge(monkeypatch,
|
|
| 458 |
prompt = str(messages[0].content)
|
| 459 |
if "SUPERVISOR FINALIZER" in prompt:
|
| 460 |
self.finalizer_calls += 1
|
| 461 |
-
return AIMessage(content="Final Answer:
|
| 462 |
self.supervisor_calls += 1
|
| 463 |
if self.supervisor_calls == 1:
|
| 464 |
return AIMessage(content='{"status":"nudge","best_answer":"","guidance":"Use the evidence to make a best guess."}')
|
|
|
|
| 465 |
return AIMessage(content='{"status":"finalize","best_answer":"Unknown","guidance":"You were already nudged. Provide your final answer based on the best available information."}')
|
| 466 |
|
| 467 |
strong = FakeStrongModel()
|
|
@@ -480,15 +479,15 @@ def test_supervisor_does_not_finalize_placeholder_after_prior_nudge(monkeypatch,
|
|
| 480 |
graph = build_react_agent(cfg)
|
| 481 |
result = graph.invoke(
|
| 482 |
{"messages": [HumanMessage(content="Question requiring a concrete answer")], "iterations": 0, "todos": []},
|
| 483 |
-
{"configurable": {"thread_id": "supervisor-
|
| 484 |
)
|
| 485 |
|
| 486 |
-
assert result["messages"][-1].content == "Final Answer:
|
| 487 |
-
assert strong.finalizer_calls ==
|
| 488 |
assert strong.supervisor_calls == 2
|
| 489 |
|
| 490 |
|
| 491 |
-
def
|
| 492 |
class FakeBoundModel:
|
| 493 |
def __init__(self):
|
| 494 |
self.calls = 0
|
|
@@ -516,18 +515,19 @@ def test_supervisor_does_not_auto_finalize_concrete_best_answer_after_prior_nudg
|
|
| 516 |
prompt = str(messages[0].content)
|
| 517 |
if "SUPERVISOR FINALIZER" in prompt:
|
| 518 |
self.finalizer_calls += 1
|
| 519 |
-
return AIMessage(content="Final Answer:
|
| 520 |
self.supervisor_calls += 1
|
| 521 |
return AIMessage(content='{"status":"nudge","best_answer":"concrete candidate","guidance":"Check one more constraint before final answer."}')
|
| 522 |
|
| 523 |
strong = FakeStrongModel()
|
| 524 |
cfg = Config.from_env()
|
| 525 |
-
cfg.recursion_limit =
|
| 526 |
cfg.budget_hard_cap = 99
|
| 527 |
cfg.budget_warn_at = 99
|
| 528 |
cfg.compact_summarize = False
|
| 529 |
monkeypatch.setenv("LILITH_HOME", str(tmp_path / ".lilith"))
|
| 530 |
monkeypatch.setattr("lilith_agent.app._SUPERVISOR_MIN_TOOL_CALLS", 1, raising=False)
|
|
|
|
| 531 |
monkeypatch.setattr("lilith_agent.app.get_extra_strong_model", lambda cfg: strong)
|
| 532 |
monkeypatch.setattr("lilith_agent.app.get_cheap_model", lambda cfg: object())
|
| 533 |
monkeypatch.setattr("lilith_agent.tools.build_tools", lambda cfg: [echo_tool])
|
|
@@ -536,12 +536,13 @@ def test_supervisor_does_not_auto_finalize_concrete_best_answer_after_prior_nudg
|
|
| 536 |
graph = build_react_agent(cfg)
|
| 537 |
result = graph.invoke(
|
| 538 |
{"messages": [HumanMessage(content="Question requiring more checking")], "iterations": 0, "todos": []},
|
| 539 |
-
{"configurable": {"thread_id": "supervisor-
|
| 540 |
)
|
| 541 |
|
| 542 |
assert strong.finalizer_calls == 0
|
| 543 |
-
assert
|
| 544 |
-
assert
|
|
|
|
| 545 |
|
| 546 |
|
| 547 |
def test_final_answer_gets_supervisor_review_and_can_be_returned_for_revision(monkeypatch, tmp_path):
|
|
|
|
| 428 |
assert strong.finalizer_calls == 1
|
| 429 |
|
| 430 |
|
| 431 |
+
def test_supervisor_finalizes_even_with_placeholder_if_requested(monkeypatch, tmp_path):
|
| 432 |
class FakeBoundModel:
|
| 433 |
def __init__(self):
|
| 434 |
self.calls = 0
|
| 435 |
|
| 436 |
def invoke(self, messages):
|
| 437 |
self.calls += 1
|
|
|
|
|
|
|
| 438 |
return _ai_with_calls([
|
| 439 |
{
|
| 440 |
"id": f"call-{self.calls}",
|
|
|
|
| 456 |
prompt = str(messages[0].content)
|
| 457 |
if "SUPERVISOR FINALIZER" in prompt:
|
| 458 |
self.finalizer_calls += 1
|
| 459 |
+
return AIMessage(content="Final Answer: finalizer output")
|
| 460 |
self.supervisor_calls += 1
|
| 461 |
if self.supervisor_calls == 1:
|
| 462 |
return AIMessage(content='{"status":"nudge","best_answer":"","guidance":"Use the evidence to make a best guess."}')
|
| 463 |
+
# On second call, it asks to finalize but with a placeholder answer. The code should allow finalization.
|
| 464 |
return AIMessage(content='{"status":"finalize","best_answer":"Unknown","guidance":"You were already nudged. Provide your final answer based on the best available information."}')
|
| 465 |
|
| 466 |
strong = FakeStrongModel()
|
|
|
|
| 479 |
graph = build_react_agent(cfg)
|
| 480 |
result = graph.invoke(
|
| 481 |
{"messages": [HumanMessage(content="Question requiring a concrete answer")], "iterations": 0, "todos": []},
|
| 482 |
+
{"configurable": {"thread_id": "supervisor-finalize-after-nudge-test"}},
|
| 483 |
)
|
| 484 |
|
| 485 |
+
assert result["messages"][-1].content == "Final Answer: finalizer output"
|
| 486 |
+
assert strong.finalizer_calls == 1
|
| 487 |
assert strong.supervisor_calls == 2
|
| 488 |
|
| 489 |
|
| 490 |
+
def test_supervisor_forces_finalize_after_max_nudges(monkeypatch, tmp_path):
|
| 491 |
class FakeBoundModel:
|
| 492 |
def __init__(self):
|
| 493 |
self.calls = 0
|
|
|
|
| 515 |
prompt = str(messages[0].content)
|
| 516 |
if "SUPERVISOR FINALIZER" in prompt:
|
| 517 |
self.finalizer_calls += 1
|
| 518 |
+
return AIMessage(content="Final Answer: max nudges forced this")
|
| 519 |
self.supervisor_calls += 1
|
| 520 |
return AIMessage(content='{"status":"nudge","best_answer":"concrete candidate","guidance":"Check one more constraint before final answer."}')
|
| 521 |
|
| 522 |
strong = FakeStrongModel()
|
| 523 |
cfg = Config.from_env()
|
| 524 |
+
cfg.recursion_limit = 20
|
| 525 |
cfg.budget_hard_cap = 99
|
| 526 |
cfg.budget_warn_at = 99
|
| 527 |
cfg.compact_summarize = False
|
| 528 |
monkeypatch.setenv("LILITH_HOME", str(tmp_path / ".lilith"))
|
| 529 |
monkeypatch.setattr("lilith_agent.app._SUPERVISOR_MIN_TOOL_CALLS", 1, raising=False)
|
| 530 |
+
monkeypatch.setattr("lilith_agent.app._SUPERVISOR_MAX_NUDGES", 5, raising=False)
|
| 531 |
monkeypatch.setattr("lilith_agent.app.get_extra_strong_model", lambda cfg: strong)
|
| 532 |
monkeypatch.setattr("lilith_agent.app.get_cheap_model", lambda cfg: object())
|
| 533 |
monkeypatch.setattr("lilith_agent.tools.build_tools", lambda cfg: [echo_tool])
|
|
|
|
| 536 |
graph = build_react_agent(cfg)
|
| 537 |
result = graph.invoke(
|
| 538 |
{"messages": [HumanMessage(content="Question requiring more checking")], "iterations": 0, "todos": []},
|
| 539 |
+
{"configurable": {"thread_id": "supervisor-max-nudges-test"}},
|
| 540 |
)
|
| 541 |
|
| 542 |
assert strong.finalizer_calls == 0
|
| 543 |
+
assert result["messages"][-1].content == "Final Answer: concrete candidate"
|
| 544 |
+
assert strong.supervisor_calls == 5
|
| 545 |
+
assert result["supervisor_decision"] == "finalize"
|
| 546 |
|
| 547 |
|
| 548 |
def test_final_answer_gets_supervisor_review_and_can_be_returned_for_revision(monkeypatch, tmp_path):
|