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 = """\
|
_RETRY_TEMPLATE = """\
|
||||||
{original_user}
|
Il JSON che hai generato contiene un errore: {previous_error}
|
||||||
|
|
||||||
--- TENTATIVO PRECEDENTE FALLITO ---
|
Correggi e rispondi di nuovo con un singolo oggetto JSON valido
|
||||||
Output: {previous_raw}
|
dentro fence ```json...```, seguendo strettamente lo schema fornito.
|
||||||
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.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
_RETRY_RAW_TRUNCATE = 800
|
|
||||||
|
|
||||||
|
|
||||||
_JSON_FENCE_RE = re.compile(
|
_JSON_FENCE_RE = re.compile(
|
||||||
r"```(?:json)?\s*(\{[\s\S]*\})\s*```",
|
r"```(?:json)?\s*(\{[\s\S]*\})\s*```",
|
||||||
@@ -429,20 +422,23 @@ class HypothesisAgent:
|
|||||||
errors: list[str] = []
|
errors: list[str] = []
|
||||||
last_raw = ""
|
last_raw = ""
|
||||||
max_attempts = 1 + self._max_retries
|
max_attempts = 1 + self._max_retries
|
||||||
|
session_id: str | None = None
|
||||||
|
|
||||||
|
try:
|
||||||
for attempt in range(max_attempts):
|
for attempt in range(max_attempts):
|
||||||
if attempt == 0:
|
if attempt == 0:
|
||||||
user = original_user
|
user = original_user
|
||||||
|
req_session_id = "new"
|
||||||
else:
|
else:
|
||||||
truncated = last_raw[:_RETRY_RAW_TRUNCATE]
|
user = _RETRY_TEMPLATE.format(previous_error=errors[-1])
|
||||||
user = _RETRY_TEMPLATE.format(
|
req_session_id = session_id or "new"
|
||||||
original_user=original_user,
|
|
||||||
previous_raw=truncated,
|
|
||||||
previous_error=errors[-1],
|
|
||||||
)
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
completion = self._llm.complete(genome, system=system, user=user)
|
completion = self._llm.complete(
|
||||||
|
genome, system=system, user=user,
|
||||||
|
session_id=req_session_id,
|
||||||
|
summarize=True,
|
||||||
|
)
|
||||||
except EmptyCompletionError as e:
|
except EmptyCompletionError as e:
|
||||||
errors.append(f"empty_completion: {e}")
|
errors.append(f"empty_completion: {e}")
|
||||||
last_raw = ""
|
last_raw = ""
|
||||||
@@ -457,6 +453,8 @@ class HypothesisAgent:
|
|||||||
continue
|
continue
|
||||||
completions.append(completion)
|
completions.append(completion)
|
||||||
last_raw = completion.text
|
last_raw = completion.text
|
||||||
|
if completion.session_id:
|
||||||
|
session_id = completion.session_id
|
||||||
|
|
||||||
strategy, err = _try_parse(completion.text)
|
strategy, err = _try_parse(completion.text)
|
||||||
if strategy is not None:
|
if strategy is not None:
|
||||||
@@ -469,6 +467,9 @@ class HypothesisAgent:
|
|||||||
)
|
)
|
||||||
assert err is not None
|
assert err is not None
|
||||||
errors.append(err)
|
errors.append(err)
|
||||||
|
finally:
|
||||||
|
if session_id:
|
||||||
|
self._llm.close_session(session_id)
|
||||||
|
|
||||||
chained = " | ".join(
|
chained = " | ".join(
|
||||||
f"attempt {i + 1}: {e}" for i, e in enumerate(errors)
|
f"attempt {i + 1}: {e}" for i, e in enumerate(errors)
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ class CompletionResult:
|
|||||||
output_tokens: int
|
output_tokens: int
|
||||||
tier: ModelTier
|
tier: ModelTier
|
||||||
model: str
|
model: str
|
||||||
|
session_id: str | None = None
|
||||||
|
|
||||||
|
|
||||||
class LLMClient:
|
class LLMClient:
|
||||||
@@ -138,6 +139,12 @@ class LLMClient:
|
|||||||
f"Request {request_id} timed out after {self._poll_timeout}s"
|
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(
|
@retry(
|
||||||
stop=stop_after_attempt(3),
|
stop=stop_after_attempt(3),
|
||||||
wait=wait_exponential(multiplier=2.0, min=2.0, max=30.0),
|
wait=wait_exponential(multiplier=2.0, min=2.0, max=30.0),
|
||||||
@@ -150,15 +157,23 @@ class LLMClient:
|
|||||||
system: str,
|
system: str,
|
||||||
user: str,
|
user: str,
|
||||||
max_tokens: int = 2000,
|
max_tokens: int = 2000,
|
||||||
|
session_id: str | None = None,
|
||||||
|
summarize: bool = False,
|
||||||
) -> CompletionResult:
|
) -> CompletionResult:
|
||||||
model = self._tier_map[genome.model_tier]
|
model = self._tier_map[genome.model_tier]
|
||||||
topic_id = self._get_or_create_topic(system)
|
topic_id = self._get_or_create_topic(system)
|
||||||
|
|
||||||
resp = self._client.post("/api/requests", json={
|
body: dict = {
|
||||||
"topic_id": topic_id,
|
"topic_id": topic_id,
|
||||||
"prompt": user,
|
"prompt": user,
|
||||||
"model": model,
|
"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:
|
if resp.status_code == 429:
|
||||||
raise OpusAgentTransientError("Rate limited")
|
raise OpusAgentTransientError("Rate limited")
|
||||||
@@ -180,4 +195,5 @@ class LLMClient:
|
|||||||
output_tokens=0,
|
output_tokens=0,
|
||||||
tier=genome.model_tier,
|
tier=genome.model_tier,
|
||||||
model=model,
|
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 {
|
return {
|
||||||
"success": True,
|
"success": True,
|
||||||
"data": {
|
"data": {
|
||||||
"id": "req-456",
|
"id": "req-456",
|
||||||
"status": "completed",
|
"status": "completed",
|
||||||
"result": text,
|
"result": text,
|
||||||
|
"session_id": session_id,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -212,3 +215,56 @@ def test_tokens_are_zero():
|
|||||||
|
|
||||||
assert out.input_tokens == 0
|
assert out.input_tokens == 0
|
||||||
assert out.output_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