diff --git a/src/multi_swarm_core/multi_swarm_core/agents/hypothesis.py b/src/multi_swarm_core/multi_swarm_core/agents/hypothesis.py index f557bdb..97c72d3 100644 --- a/src/multi_swarm_core/multi_swarm_core/agents/hypothesis.py +++ b/src/multi_swarm_core/multi_swarm_core/agents/hypothesis.py @@ -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) diff --git a/src/multi_swarm_core/multi_swarm_core/llm/client.py b/src/multi_swarm_core/multi_swarm_core/llm/client.py index 790c98e..13a4f04 100644 --- a/src/multi_swarm_core/multi_swarm_core/llm/client.py +++ b/src/multi_swarm_core/multi_swarm_core/llm/client.py @@ -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"), ) diff --git a/src/multi_swarm_core/tests/unit/test_llm_client.py b/src/multi_swarm_core/tests/unit/test_llm_client.py index ac47250..9787aed 100644 --- a/src/multi_swarm_core/tests/unit/test_llm_client.py +++ b/src/multi_swarm_core/tests/unit/test_llm_client.py @@ -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]