diff --git a/src/landppt/services/slide/slide_edit_agent_service.py b/src/landppt/services/slide/slide_edit_agent_service.py index 383dd2f..e05a653 100644 --- a/src/landppt/services/slide/slide_edit_agent_service.py +++ b/src/landppt/services/slide/slide_edit_agent_service.py @@ -15,6 +15,10 @@ from pydantic import BaseModel AgentEditMode = Literal["slide", "element"] +_MAX_CONVERSATION_HISTORY_MESSAGES = 10 +_MAX_CONVERSATION_HISTORY_TOTAL_CHARS = 6000 +_MAX_CONVERSATION_HISTORY_MESSAGE_CHARS = 1200 + class SlideEditAgentRequest(BaseModel): projectId: str @@ -1008,6 +1012,7 @@ class SlideEditAgentService: "selected_element_id": request.selectedElementId, "selected_element_html": request.selectedElementHtml, "user_request": request.userRequest, + "conversation_history": self._conversation_history_context(request), "current_html": runner.current_html, "vision": self._vision_context(request), "available_tools": self._tool_schemas(runner), @@ -1016,10 +1021,59 @@ class SlideEditAgentService: } return ( "Use a ReAct loop to edit this PPT slide. Choose exactly one action. " - "Use final when the draft is ready. Return strict JSON only.\n\n" + "Use conversation_history for continuity, but execute the latest " + "user_request. Use final when the draft is ready. Return strict JSON only.\n\n" + json.dumps(context, ensure_ascii=False, indent=2) ) + def _conversation_history_context( + self, request: SlideEditAgentRequest + ) -> List[Dict[str, str]]: + cleaned: List[Dict[str, str]] = [] + total_chars = 0 + + for item in reversed(request.chatHistory or []): + if len(cleaned) >= _MAX_CONVERSATION_HISTORY_MESSAGES: + break + if not isinstance(item, dict): + continue + + role = str(item.get("role") or "").strip().lower() + if role not in {"user", "assistant"}: + continue + + content = str(item.get("content") or "").strip() + if not content: + continue + + remaining_chars = _MAX_CONVERSATION_HISTORY_TOTAL_CHARS - total_chars + if remaining_chars <= 0: + break + + max_chars = min( + remaining_chars, + _MAX_CONVERSATION_HISTORY_MESSAGE_CHARS, + ) + content = self._truncate_history_content(content, max_chars) + if not content: + continue + + cleaned.append({"role": role, "content": content}) + total_chars += len(content) + + cleaned.reverse() + return cleaned + + @staticmethod + def _truncate_history_content(content: str, max_chars: int) -> str: + if max_chars <= 0: + return "" + if len(content) <= max_chars: + return content + if max_chars <= 3: + return content[:max_chars] + return content[: max_chars - 3].rstrip() + "..." + def _vision_context(self, request: SlideEditAgentRequest) -> Dict[str, Any]: attachments: List[Dict[str, Any]] = [] if request.slideScreenshot: diff --git a/tests/test_slide_edit_agent_service.py b/tests/test_slide_edit_agent_service.py index e32243c..5085326 100644 --- a/tests/test_slide_edit_agent_service.py +++ b/tests/test_slide_edit_agent_service.py @@ -381,6 +381,53 @@ def test_tool_runner_build_proposal_strips_agent_ids(): assert proposal.validation.valid is True +def _agent_prompt_context(service: SlideEditAgentService, request: SlideEditAgentRequest): + runner = SlideEditToolRunner(SlideEditAgentContext.from_request(request)) + prompt = service._build_prompt(request, runner, scratchpad=[], max_iterations=6) + return json.loads(prompt.split("\n\n", 1)[1]) + + +def test_slide_edit_agent_prompt_includes_sanitized_conversation_history(): + service = SlideEditAgentService() + request = _tool_request( + chatHistory=[ + {"role": "system", "content": "Do not include this."}, + {"role": "user", "content": "Make the heading shorter first."}, + {"role": "assistant", "content": "I shortened the heading."}, + {"role": "tool", "content": "Internal tool output."}, + {"role": "assistant", "content": " "}, + ] + ) + + context = _agent_prompt_context(service, request) + + assert context["conversation_history"] == [ + {"role": "user", "content": "Make the heading shorter first."}, + {"role": "assistant", "content": "I shortened the heading."}, + ] + assert context["user_request"] == "Make the title shorter" + + +def test_slide_edit_agent_prompt_limits_conversation_history_to_recent_messages(): + service = SlideEditAgentService() + history = [ + {"role": "user", "content": f"message {index}"} + for index in range(14) + ] + history.append({"role": "assistant", "content": "x" * 1500}) + request = _tool_request(chatHistory=history) + + context = _agent_prompt_context(service, request) + conversation_history = context["conversation_history"] + + assert len(conversation_history) == 10 + assert conversation_history[0] == {"role": "user", "content": "message 5"} + assert conversation_history[-2] == {"role": "user", "content": "message 13"} + assert conversation_history[-1]["role"] == "assistant" + assert conversation_history[-1]["content"].endswith("...") + assert len(conversation_history[-1]["content"]) <= 1200 + + class _FakePPTService: def __init__(self, responses): self.responses = list(responses)