mirror of
https://github.com/tiennm99/DocsGPT.git
synced 2026-10-18 02:14:06 +00:00
fix: more stability for durable tasks, retry strategy, refactor dead
code
This commit is contained in:
1 parent
114585cd7d
commit
e96ff8658c
27 files changed
+1060
-162
No files matched your search
@@ -119,6 +119,62 @@ class TestErrorNamesWhatTheModelCanCall:
|
||||
_events, (result, _call_id) = _drain(executor, _call("make_a_pdf"), tools=tools_dict)
|
||||
assert "create_artifact" in result
|
||||
|
||||
def test_only_advertises_tools_in_scope_for_this_call(self):
|
||||
"""A narrowed ``tools_dict`` must not be told about out-of-scope tools.
|
||||
|
||||
The error string is a tool RESULT handed straight back to the model, so
|
||||
naming a tool it cannot call this round just buys another failed round.
|
||||
"""
|
||||
executor = ToolExecutor()
|
||||
wide = {
|
||||
f"t{n}": {
|
||||
"name": f"server_{n}",
|
||||
"actions": [
|
||||
{
|
||||
"name": f"action_{n}",
|
||||
"description": "D",
|
||||
"active": True,
|
||||
"parameters": {"properties": {}},
|
||||
}
|
||||
],
|
||||
}
|
||||
for n in range(4)
|
||||
}
|
||||
executor.prepare_tools_for_llm(wide)
|
||||
narrowed = {"t1": wide["t1"]}
|
||||
_events, (result, _call_id) = _drain(
|
||||
executor, _call("make_a_pdf"), tools=narrowed
|
||||
)
|
||||
assert "action_1" in result, result
|
||||
for out_of_scope in ("action_0", "action_2", "action_3"):
|
||||
assert out_of_scope not in result, (out_of_scope, result)
|
||||
|
||||
def test_the_advertised_list_is_capped(self):
|
||||
"""This string joins the message history and is re-sent every round.
|
||||
|
||||
Uncapped, a large MCP fleet turns a single failed call into kilobytes
|
||||
of prose duplicating the tool schema the provider already has.
|
||||
"""
|
||||
executor = ToolExecutor()
|
||||
many = {
|
||||
f"t{n}": {
|
||||
"name": f"server_{n}",
|
||||
"actions": [
|
||||
{
|
||||
"name": f"action_{n:03d}",
|
||||
"description": "D",
|
||||
"active": True,
|
||||
"parameters": {"properties": {}},
|
||||
}
|
||||
],
|
||||
}
|
||||
for n in range(150)
|
||||
}
|
||||
executor.prepare_tools_for_llm(many)
|
||||
_events, (result, _call_id) = _drain(executor, _call("make_a_pdf"), tools=many)
|
||||
assert "and 120 more" in result, result
|
||||
assert len(result) < 1000, len(result)
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestThrottleScope:
|
||||
@@ -167,14 +223,27 @@ class TestThrottleScope:
|
||||
assert "has already failed" in result, result
|
||||
assert "(none available)" in result, result
|
||||
|
||||
def test_distinct_payloads_for_an_invented_name_stay_distinct(self):
|
||||
def test_varying_payloads_for_an_invented_name_still_trip_the_guard(self):
|
||||
"""Varying the arguments must not reset the count for an unknown name.
|
||||
|
||||
Told a call failed, a model's natural next move is to adjust its
|
||||
arguments. Keying the counter on name+payload minted a fresh signature
|
||||
every round, so the guard never fired and the turn ran to
|
||||
``MAX_TOOL_ITERATIONS``. Arguments cannot rescue an unknown name:
|
||||
``ToolActionParser`` resolves from ``call.name`` alone.
|
||||
"""
|
||||
executor = ToolExecutor()
|
||||
results = []
|
||||
for index, body in enumerate(['{"a": 1}', '{"b": 2}', '{"c": 3}']):
|
||||
_events, (result, _call_id) = _drain(
|
||||
executor, _call("note_view", arguments=body, call_id=f"c{index}")
|
||||
)
|
||||
assert "has already failed" not in result, (index, result)
|
||||
assert len(executor._unresolvable_calls) == 3
|
||||
results.append(result)
|
||||
assert "has already failed" not in results[0], results[0]
|
||||
assert "has already failed" not in results[1], results[1]
|
||||
assert "has already failed 2 times" in results[2], results[2]
|
||||
# One counter for the name, not one per payload.
|
||||
assert list(executor._unresolvable_calls) == ["note_view"]
|
||||
|
||||
def test_the_failure_count_keeps_escalating(self):
|
||||
"""A count frozen at the limit makes the message and the ops log useless."""
|
||||
|
||||
@@ -39,17 +39,17 @@ def _tool():
|
||||
|
||||
|
||||
def test_validate_accepts_minimal_presentation():
|
||||
assert _tool()._validate("presentation", {"slides": [{"title": "x"}]}) is None
|
||||
assert _tool()._validate("presentation", {"slides": [{"title": "x"}]})[1] is None
|
||||
|
||||
|
||||
def test_validate_rejects_missing_required_key():
|
||||
err = _tool()._validate("presentation", {"title": "no slides"})
|
||||
_spec, err = _tool()._validate("presentation", {"title": "no slides"})
|
||||
assert err["status"] == "error"
|
||||
assert "invalid presentation spec" in err["error"]
|
||||
|
||||
|
||||
def test_validate_rejects_unknown_key():
|
||||
err = _tool()._validate("document", {"sections": [], "bogus": 1})
|
||||
_spec, err = _tool()._validate("document", {"sections": [], "bogus": 1})
|
||||
assert err["status"] == "error"
|
||||
|
||||
|
||||
@@ -97,12 +97,12 @@ def test_reversion_without_title_falls_back_to_generic(monkeypatch):
|
||||
|
||||
|
||||
def test_validate_rejects_wrong_type():
|
||||
err = _tool()._validate("spreadsheet", {"sheets": "not-a-list"})
|
||||
_spec, err = _tool()._validate("spreadsheet", {"sheets": "not-a-list"})
|
||||
assert err["status"] == "error"
|
||||
|
||||
|
||||
def test_validate_rejects_non_object_spec():
|
||||
err = _tool()._validate("pdf", "just a string")
|
||||
_spec, err = _tool()._validate("pdf", "just a string")
|
||||
assert err["status"] == "error"
|
||||
assert "spec must be a JSON object" in err["error"]
|
||||
|
||||
@@ -300,16 +300,16 @@ def test_validate_accepts_html_spec():
|
||||
{"type": "code", "text": "print(1)"},
|
||||
],
|
||||
}
|
||||
assert _tool()._validate("html", spec) is None
|
||||
assert _tool()._validate("html", spec)[1] is None
|
||||
|
||||
|
||||
def test_validate_rejects_html_unknown_block_key():
|
||||
err = _tool()._validate("html", {"blocks": [{"type": "paragraph", "text": "x", "bogus": 1}]})
|
||||
_spec, err = _tool()._validate("html", {"blocks": [{"type": "paragraph", "text": "x", "bogus": 1}]})
|
||||
assert err["status"] == "error"
|
||||
|
||||
|
||||
def test_validate_rejects_html_bad_heading_level():
|
||||
err = _tool()._validate("html", {"blocks": [{"type": "heading", "text": "x", "level": 9}]})
|
||||
_spec, err = _tool()._validate("html", {"blocks": [{"type": "heading", "text": "x", "level": 9}]})
|
||||
assert err["status"] == "error"
|
||||
|
||||
|
||||
@@ -480,19 +480,28 @@ def test_validate_accepts_a_json_encoded_spec_string():
|
||||
"""
|
||||
tool = _tool()
|
||||
spec = json.dumps({"blocks": [{"type": "heading", "text": "Hi"}]})
|
||||
assert tool._validate("pdf", spec) is None
|
||||
assert tool._validate("pdf", spec)[1] is None
|
||||
|
||||
|
||||
def test_validate_coerces_the_string_spec_for_the_caller():
|
||||
"""The coerced spec must come back, not just the verdict.
|
||||
|
||||
Returning only the error left the caller holding the original string: it
|
||||
passed validation and was then written to spec.json as a JSON *string*,
|
||||
which the renderer cannot read.
|
||||
"""
|
||||
tool = _tool()
|
||||
spec = json.dumps({"sections": [{"heading": "H", "paragraphs": ["p"]}]})
|
||||
assert tool._coerce_spec(spec) == {"sections": [{"heading": "H", "paragraphs": ["p"]}]}
|
||||
raw = json.dumps({"sections": [{"heading": "H", "paragraphs": ["p"]}]})
|
||||
spec, err = tool._validate("document", raw)
|
||||
assert err is None
|
||||
assert spec == {"sections": [{"heading": "H", "paragraphs": ["p"]}]}
|
||||
assert tool._coerce_spec(raw) == spec
|
||||
|
||||
|
||||
def test_validate_still_rejects_a_json_string_that_is_not_an_object():
|
||||
tool = _tool()
|
||||
assert tool._validate("pdf", json.dumps(["not", "an", "object"]))["status"] == "error"
|
||||
assert tool._validate("pdf", "not json at all")["status"] == "error"
|
||||
assert tool._validate("pdf", json.dumps(["not", "an", "object"]))[1]["status"] == "error"
|
||||
assert tool._validate("pdf", "not json at all")[1]["status"] == "error"
|
||||
|
||||
|
||||
def test_create_accepts_a_stringified_spec(monkeypatch):
|
||||
@@ -525,12 +534,12 @@ def test_pdf_schema_accepts_heading_level():
|
||||
{"type": "paragraph", "text": "Body."},
|
||||
],
|
||||
}
|
||||
assert _tool()._validate("pdf", spec) is None
|
||||
assert _tool()._validate("pdf", spec)[1] is None
|
||||
|
||||
|
||||
def test_pdf_schema_still_rejects_an_out_of_range_level():
|
||||
spec = {"blocks": [{"type": "heading", "text": "x", "level": 9}]}
|
||||
assert _tool()._validate("pdf", spec)["status"] == "error"
|
||||
assert _tool()._validate("pdf", spec)[1]["status"] == "error"
|
||||
|
||||
|
||||
def test_pdf_renderer_honours_heading_level():
|
||||
|
||||
Reference in new issue
Block a user