feat(llm): add session_id for retries and summarize for run tracking
HypothesisAgent now opens an OpusAgent session on first attempt and reuses it for parse-error retries — Claude sees full conversation context instead of re-stuffing the prompt. Sessions are closed after propose() completes. All completions set summarize=true so topics accumulate incremental summaries of GA activity. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -288,19 +288,12 @@ def _render_focus_block(keys: list[str], market: MarketSummary) -> str:
|
||||
|
||||
|
||||
_RETRY_TEMPLATE = """\
|
||||
{original_user}
|
||||
Il JSON che hai generato contiene un errore: {previous_error}
|
||||
|
||||
--- TENTATIVO PRECEDENTE FALLITO ---
|
||||
Output: {previous_raw}
|
||||
Errore: {previous_error}
|
||||
---
|
||||
Correggi l'errore e rispondi di nuovo con un singolo oggetto JSON valido
|
||||
dentro fence ```json...```, seguendo strettamente lo schema fornito nel
|
||||
SYSTEM message.
|
||||
Correggi e rispondi di nuovo con un singolo oggetto JSON valido
|
||||
dentro fence ```json...```, seguendo strettamente lo schema fornito.
|
||||
"""
|
||||
|
||||
_RETRY_RAW_TRUNCATE = 800
|
||||
|
||||
|
||||
_JSON_FENCE_RE = re.compile(
|
||||
r"```(?:json)?\s*(\{[\s\S]*\})\s*```",
|
||||
@@ -429,46 +422,54 @@ class HypothesisAgent:
|
||||
errors: list[str] = []
|
||||
last_raw = ""
|
||||
max_attempts = 1 + self._max_retries
|
||||
session_id: str | None = None
|
||||
|
||||
for attempt in range(max_attempts):
|
||||
if attempt == 0:
|
||||
user = original_user
|
||||
else:
|
||||
truncated = last_raw[:_RETRY_RAW_TRUNCATE]
|
||||
user = _RETRY_TEMPLATE.format(
|
||||
original_user=original_user,
|
||||
previous_raw=truncated,
|
||||
previous_error=errors[-1],
|
||||
)
|
||||
try:
|
||||
for attempt in range(max_attempts):
|
||||
if attempt == 0:
|
||||
user = original_user
|
||||
req_session_id = "new"
|
||||
else:
|
||||
user = _RETRY_TEMPLATE.format(previous_error=errors[-1])
|
||||
req_session_id = session_id or "new"
|
||||
|
||||
try:
|
||||
completion = self._llm.complete(genome, system=system, user=user)
|
||||
except EmptyCompletionError as e:
|
||||
errors.append(f"empty_completion: {e}")
|
||||
last_raw = ""
|
||||
continue
|
||||
except OpusAgentTransientError as e:
|
||||
errors.append(f"transient_error: {e}")
|
||||
last_raw = ""
|
||||
continue
|
||||
except OpusAgentError as e:
|
||||
errors.append(f"opus_agent_error: {e}")
|
||||
last_raw = ""
|
||||
continue
|
||||
completions.append(completion)
|
||||
last_raw = completion.text
|
||||
try:
|
||||
completion = self._llm.complete(
|
||||
genome, system=system, user=user,
|
||||
session_id=req_session_id,
|
||||
summarize=True,
|
||||
)
|
||||
except EmptyCompletionError as e:
|
||||
errors.append(f"empty_completion: {e}")
|
||||
last_raw = ""
|
||||
continue
|
||||
except OpusAgentTransientError as e:
|
||||
errors.append(f"transient_error: {e}")
|
||||
last_raw = ""
|
||||
continue
|
||||
except OpusAgentError as e:
|
||||
errors.append(f"opus_agent_error: {e}")
|
||||
last_raw = ""
|
||||
continue
|
||||
completions.append(completion)
|
||||
last_raw = completion.text
|
||||
if completion.session_id:
|
||||
session_id = completion.session_id
|
||||
|
||||
strategy, err = _try_parse(completion.text)
|
||||
if strategy is not None:
|
||||
return HypothesisProposal(
|
||||
strategy=strategy,
|
||||
raw_text=completion.text,
|
||||
completions=completions,
|
||||
parse_error=None,
|
||||
n_attempts=len(completions),
|
||||
)
|
||||
assert err is not None
|
||||
errors.append(err)
|
||||
strategy, err = _try_parse(completion.text)
|
||||
if strategy is not None:
|
||||
return HypothesisProposal(
|
||||
strategy=strategy,
|
||||
raw_text=completion.text,
|
||||
completions=completions,
|
||||
parse_error=None,
|
||||
n_attempts=len(completions),
|
||||
)
|
||||
assert err is not None
|
||||
errors.append(err)
|
||||
finally:
|
||||
if session_id:
|
||||
self._llm.close_session(session_id)
|
||||
|
||||
chained = " | ".join(
|
||||
f"attempt {i + 1}: {e}" for i, e in enumerate(errors)
|
||||
|
||||
@@ -54,6 +54,7 @@ class CompletionResult:
|
||||
output_tokens: int
|
||||
tier: ModelTier
|
||||
model: str
|
||||
session_id: str | None = None
|
||||
|
||||
|
||||
class LLMClient:
|
||||
@@ -138,6 +139,12 @@ class LLMClient:
|
||||
f"Request {request_id} timed out after {self._poll_timeout}s"
|
||||
)
|
||||
|
||||
def close_session(self, session_id: str) -> None:
|
||||
try:
|
||||
self._client.delete(f"/api/sessions/{session_id}")
|
||||
except httpx.HTTPError:
|
||||
logger.debug("Failed to close session %s", session_id)
|
||||
|
||||
@retry(
|
||||
stop=stop_after_attempt(3),
|
||||
wait=wait_exponential(multiplier=2.0, min=2.0, max=30.0),
|
||||
@@ -150,15 +157,23 @@ class LLMClient:
|
||||
system: str,
|
||||
user: str,
|
||||
max_tokens: int = 2000,
|
||||
session_id: str | None = None,
|
||||
summarize: bool = False,
|
||||
) -> CompletionResult:
|
||||
model = self._tier_map[genome.model_tier]
|
||||
topic_id = self._get_or_create_topic(system)
|
||||
|
||||
resp = self._client.post("/api/requests", json={
|
||||
body: dict = {
|
||||
"topic_id": topic_id,
|
||||
"prompt": user,
|
||||
"model": model,
|
||||
})
|
||||
}
|
||||
if session_id is not None:
|
||||
body["session_id"] = session_id
|
||||
if summarize:
|
||||
body["summarize"] = True
|
||||
|
||||
resp = self._client.post("/api/requests", json=body)
|
||||
|
||||
if resp.status_code == 429:
|
||||
raise OpusAgentTransientError("Rate limited")
|
||||
@@ -180,4 +195,5 @@ class LLMClient:
|
||||
output_tokens=0,
|
||||
tier=genome.model_tier,
|
||||
model=model,
|
||||
session_id=result.get("session_id"),
|
||||
)
|
||||
|
||||
@@ -34,13 +34,16 @@ REQUEST_ACCEPTED = {
|
||||
}
|
||||
|
||||
|
||||
def _completed_response(text: str = "(strategy ...)") -> dict:
|
||||
def _completed_response(
|
||||
text: str = "(strategy ...)", session_id: str = "sess-789",
|
||||
) -> dict:
|
||||
return {
|
||||
"success": True,
|
||||
"data": {
|
||||
"id": "req-456",
|
||||
"status": "completed",
|
||||
"result": text,
|
||||
"session_id": session_id,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -212,3 +215,56 @@ def test_tokens_are_zero():
|
||||
|
||||
assert out.input_tokens == 0
|
||||
assert out.output_tokens == 0
|
||||
|
||||
|
||||
def test_session_id_returned_from_completion():
|
||||
transport = _mock_transport([
|
||||
httpx.Response(201, json=TOPIC_RESPONSE),
|
||||
httpx.Response(202, json=REQUEST_ACCEPTED),
|
||||
httpx.Response(200, json=_completed_response(session_id="sess-abc")),
|
||||
])
|
||||
client = _make_client(transport)
|
||||
out = client.complete(make_genome(ModelTier.C), system="sys", user="usr", session_id="new")
|
||||
|
||||
assert out.session_id == "sess-abc"
|
||||
|
||||
|
||||
def test_session_id_and_summarize_sent_in_request():
|
||||
requests_seen: list[dict] = []
|
||||
|
||||
def handler(request: httpx.Request) -> httpx.Response:
|
||||
if request.method == "POST" and "/requests" in str(request.url):
|
||||
import json
|
||||
requests_seen.append(json.loads(request.content))
|
||||
return httpx.Response(202, json=REQUEST_ACCEPTED)
|
||||
if request.method == "POST" and "/topics" in str(request.url):
|
||||
return httpx.Response(201, json=TOPIC_RESPONSE)
|
||||
return httpx.Response(200, json=_completed_response())
|
||||
|
||||
transport = httpx.MockTransport(handler)
|
||||
client = _make_client(transport)
|
||||
client.complete(
|
||||
make_genome(ModelTier.C), system="sys", user="usr",
|
||||
session_id="sess-existing", summarize=True,
|
||||
)
|
||||
|
||||
assert len(requests_seen) == 1
|
||||
assert requests_seen[0]["session_id"] == "sess-existing"
|
||||
assert requests_seen[0]["summarize"] is True
|
||||
|
||||
|
||||
def test_close_session():
|
||||
deleted: list[str] = []
|
||||
|
||||
def handler(request: httpx.Request) -> httpx.Response:
|
||||
if request.method == "DELETE":
|
||||
deleted.append(str(request.url))
|
||||
return httpx.Response(200, json={"success": True})
|
||||
return httpx.Response(200, json={"success": True})
|
||||
|
||||
transport = httpx.MockTransport(handler)
|
||||
client = _make_client(transport)
|
||||
client.close_session("sess-to-close")
|
||||
|
||||
assert len(deleted) == 1
|
||||
assert "sess-to-close" in deleted[0]
|
||||
|
||||
Reference in New Issue
Block a user