fix codegen timeout
This commit is contained in:
@@ -209,8 +209,11 @@ unit tests (24) + box integration tests (2).
|
|||||||
body…" badge) → sync codegen write → `materialize_skill` → hot-merge into the
|
body…" badge) → sync codegen write → `materialize_skill` → hot-merge into the
|
||||||
tree → the new leaf runs directly so the request is answered. `create_category`
|
tree → the new leaf runs directly so the request is answered. `create_category`
|
||||||
runs the same chain after authoring the category (`create_category` →
|
runs the same chain after authoring the category (`create_category` →
|
||||||
`create_skill` → run). Codegen failure leaves a navigable stub and returns a
|
`create_skill` → run). Codegen failure — including a request timeout — leaves
|
||||||
graceful `create_skill` result.
|
a navigable stub and returns a graceful `create_skill` result; a timeout is
|
||||||
|
raised as `CodegenError` by the client, never a raw `TimeoutError`. The
|
||||||
|
default codegen timeout is 1200s (`cli.build_scheduler`); raise
|
||||||
|
`codegen.timeout` in config for harder prompts.
|
||||||
|
|
||||||
### Code principles
|
### Code principles
|
||||||
- **No mocking.** The decision engine is always real SemIf; the LLM is always a
|
- **No mocking.** The decision engine is always real SemIf; the LLM is always a
|
||||||
|
|||||||
+1
-1
@@ -52,7 +52,7 @@ def build_scheduler(config: dict) -> tuple[Scheduler, dict]:
|
|||||||
"base_url", config.get("llm", {}).get("base_url", "http://localhost:11434/v1")
|
"base_url", config.get("llm", {}).get("base_url", "http://localhost:11434/v1")
|
||||||
),
|
),
|
||||||
model=codegen_cfg.get("model", "qwen38-iq3s"),
|
model=codegen_cfg.get("model", "qwen38-iq3s"),
|
||||||
timeout=float(codegen_cfg.get("timeout", 600.0)),
|
timeout=float(codegen_cfg.get("timeout", 1200.0)),
|
||||||
)
|
)
|
||||||
log = DecisionLog(config.get("log", "data/decisions.jsonl"))
|
log = DecisionLog(config.get("log", "data/decisions.jsonl"))
|
||||||
trace = TraceLog(config.get("trace", "data/runs.jsonl"))
|
trace = TraceLog(config.get("trace", "data/runs.jsonl"))
|
||||||
|
|||||||
@@ -72,6 +72,10 @@ class CodegenClient:
|
|||||||
raise CodegenError(
|
raise CodegenError(
|
||||||
f"codegen endpoint unreachable at {url}: {exc}. Is your local server running?"
|
f"codegen endpoint unreachable at {url}: {exc}. Is your local server running?"
|
||||||
) from exc
|
) from exc
|
||||||
|
except TimeoutError as exc:
|
||||||
|
raise CodegenError(
|
||||||
|
f"codegen request timed out after {self.timeout}s at {url}"
|
||||||
|
) from exc
|
||||||
return payload["choices"][0]["message"]["content"]
|
return payload["choices"][0]["message"]["content"]
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -162,7 +162,7 @@ def test_generate_skill_body_codegen(tmp_path):
|
|||||||
client = CodegenClient(
|
client = CodegenClient(
|
||||||
base_url=codegen_cfg.get("base_url", "http://localhost:11434/v1"),
|
base_url=codegen_cfg.get("base_url", "http://localhost:11434/v1"),
|
||||||
model=codegen_cfg.get("model", "qwen38-iq3s"),
|
model=codegen_cfg.get("model", "qwen38-iq3s"),
|
||||||
timeout=float(codegen_cfg.get("timeout", 600.0)),
|
timeout=float(codegen_cfg.get("timeout", 1200.0)),
|
||||||
)
|
)
|
||||||
tree = build_tree(build_skills({"skills": {}}))
|
tree = build_tree(build_skills({"skills": {}}))
|
||||||
draft = SkillDraft(
|
draft = SkillDraft(
|
||||||
@@ -196,6 +196,7 @@ def test_create_skill_empty_category_does_not_wedge(tmp_path):
|
|||||||
config["log"] = str(tmp_path / "decisions.jsonl")
|
config["log"] = str(tmp_path / "decisions.jsonl")
|
||||||
config["trace"] = str(tmp_path / "runs.jsonl")
|
config["trace"] = str(tmp_path / "runs.jsonl")
|
||||||
scheduler, config = build_scheduler(config)
|
scheduler, config = build_scheduler(config)
|
||||||
|
scheduler.codegen = None # wedge regression only; skip the ~7min codegen body write
|
||||||
scheduler.tree["travel_planning"] = []
|
scheduler.tree["travel_planning"] = []
|
||||||
|
|
||||||
for _ in range(2):
|
for _ in range(2):
|
||||||
|
|||||||
+32
-1
@@ -8,12 +8,14 @@ the CodegenClient itself is real, not mocked.
|
|||||||
|
|
||||||
import json
|
import json
|
||||||
import threading
|
import threading
|
||||||
|
import time
|
||||||
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from semif_agent.codegen import (
|
from semif_agent.codegen import (
|
||||||
CodegenClient,
|
CodegenClient,
|
||||||
|
CodegenError,
|
||||||
build_skill_body_prompt,
|
build_skill_body_prompt,
|
||||||
generate_skill_body,
|
generate_skill_body,
|
||||||
parse_skill_body,
|
parse_skill_body,
|
||||||
@@ -217,10 +219,39 @@ def test_codegen_client_unreachable_raises(tmp_path):
|
|||||||
client = CodegenClient(base_url="http://127.0.0.1:1/v1", model="test", timeout=2)
|
client = CodegenClient(base_url="http://127.0.0.1:1/v1", model="test", timeout=2)
|
||||||
tree = build_tree(build_skills({"skills": {}}))
|
tree = build_tree(build_skills({"skills": {}}))
|
||||||
draft = SkillDraft(name="probe", description="Probe the service.")
|
draft = SkillDraft(name="probe", description="Probe the service.")
|
||||||
with pytest.raises(Exception):
|
with pytest.raises(CodegenError):
|
||||||
generate_skill_body(client, Request("is the service up?"), "tracking", draft, tree)
|
generate_skill_body(client, Request("is the service up?"), "tracking", draft, tree)
|
||||||
|
|
||||||
|
|
||||||
|
class _SilentOpenAI(BaseHTTPRequestHandler):
|
||||||
|
"""Accepts the request but never replies; the client must time out."""
|
||||||
|
|
||||||
|
def do_POST(self):
|
||||||
|
length = int(self.headers.get("Content-Length") or 0)
|
||||||
|
self.rfile.read(length)
|
||||||
|
time.sleep(5)
|
||||||
|
|
||||||
|
def log_message(self, format, *args):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def test_chat_timeout_raises_codegen_error():
|
||||||
|
handler = type("Handler", (_SilentOpenAI,), {})
|
||||||
|
httpd = ThreadingHTTPServer(("127.0.0.1", 0), handler)
|
||||||
|
threading.Thread(target=httpd.serve_forever, daemon=True).start()
|
||||||
|
try:
|
||||||
|
client = CodegenClient(
|
||||||
|
base_url=f"http://127.0.0.1:{httpd.server_address[1]}/v1",
|
||||||
|
model="test",
|
||||||
|
timeout=0.5,
|
||||||
|
)
|
||||||
|
with pytest.raises(CodegenError, match="timed out"):
|
||||||
|
client.chat([{"role": "user", "content": "hi"}])
|
||||||
|
finally:
|
||||||
|
httpd.shutdown()
|
||||||
|
httpd.server_close()
|
||||||
|
|
||||||
|
|
||||||
def test_chat_omits_max_tokens_by_default():
|
def test_chat_omits_max_tokens_by_default():
|
||||||
httpd, base = _fake_server(GOOD_BODY)
|
httpd, base = _fake_server(GOOD_BODY)
|
||||||
try:
|
try:
|
||||||
|
|||||||
Reference in New Issue
Block a user