diff --git a/autogpt_platform/backend/backend/api/features/chat/sdk/service.py b/autogpt_platform/backend/backend/api/features/chat/sdk/service.py
index b32bd0992f..28ee1f5dba 100644
--- a/autogpt_platform/backend/backend/api/features/chat/sdk/service.py
+++ b/autogpt_platform/backend/backend/api/features/chat/sdk/service.py
@@ -1,7 +1,6 @@
"""Claude Agent SDK service layer for CoPilot chat completions."""
import asyncio
-import glob
import json
import logging
import os
@@ -9,12 +8,6 @@ import uuid
from collections.abc import AsyncGenerator
from typing import Any
-import openai
-
-from backend.data.understanding import (
- format_understanding_for_prompt,
- get_business_understanding,
-)
from backend.util.exceptions import NotFoundError
from ..config import ChatConfig
@@ -34,11 +27,11 @@ from ..response_model import (
StreamToolInputAvailable,
StreamToolOutputAvailable,
)
+from ..service import _build_system_prompt, _generate_session_title
from ..tracking import track_user_message
from .anthropic_fallback import stream_with_anthropic
from .response_adapter import SDKResponseAdapter
from .security_hooks import create_security_hooks
-from .session_file import cleanup_session_file, write_session_file
from .tool_adapter import (
COPILOT_TOOL_NAMES,
create_copilot_mcp_server,
@@ -51,152 +44,121 @@ config = ChatConfig()
# Set to hold background tasks to prevent garbage collection
_background_tasks: set[asyncio.Task[Any]] = set()
-# SDK tool-results directory pattern
-_SDK_TOOL_RESULTS_GLOB = "/root/.claude/projects/*/tool-results/*"
+# SDK tool-results glob pattern — clean these up after each query
+_SDK_TOOL_RESULTS_GLOB = os.path.expanduser("~/.claude/projects/*/tool-results/*")
def _cleanup_sdk_tool_results() -> None:
"""Remove SDK tool-result files to prevent disk accumulation."""
- for path in glob.glob(_SDK_TOOL_RESULTS_GLOB):
+ import glob as _glob
+
+ for path in _glob.glob(_SDK_TOOL_RESULTS_GLOB):
try:
os.remove(path)
except OSError:
pass
-DEFAULT_SYSTEM_PROMPT = """You are **Otto**, an AI Co-Pilot for AutoGPT and a Forward-Deployed Automation Engineer serving small business owners. Your mission is to help users automate business tasks with AI by delivering tangible value through working automations—not through documentation or lengthy explanations.
+async def _compress_conversation_history(
+ session: ChatSession,
+) -> list[ChatMessage]:
+ """Compress prior conversation messages if they exceed the token threshold.
-Here is everything you know about the current user from previous interactions:
+ Uses the shared compress_context() from prompt.py which supports:
+ - LLM summarization of old messages (keeps recent ones intact)
+ - Progressive content truncation as fallback
+ - Middle-out deletion as last resort
-
-{users_information}
-
-
-## YOUR CORE MANDATE
-
-You are action-oriented. Your success is measured by:
-- **Value Delivery**: Does the user think "wow, that was amazing" or "what was the point"?
-- **Demonstrable Proof**: Show working automations, not descriptions of what's possible
-- **Time Saved**: Focus on tangible efficiency gains
-- **Quality Output**: Deliver results that meet or exceed expectations
-
-## YOUR WORKFLOW
-
-Adapt flexibly to the conversation context. Not every interaction requires all stages:
-
-1. **Explore & Understand**: Learn about the user's business, tasks, and goals. Use `add_understanding` to capture important context that will improve future conversations.
-
-2. **Assess Automation Potential**: Help the user understand whether and how AI can automate their task.
-
-3. **Prepare for AI**: Provide brief, actionable guidance on prerequisites (data, access, etc.).
-
-4. **Discover or Create Agents**:
- - **Always check the user's library first** with `find_library_agent` (these may be customized to their needs)
- - Search the marketplace with `find_agent` for pre-built automations
- - Find reusable components with `find_block`
- - Create custom solutions with `create_agent` if nothing suitable exists
- - Modify existing library agents with `edit_agent`
-
-5. **Execute**: Run automations immediately, schedule them, or set up webhooks using `run_agent`. Test specific components with `run_block`.
-
-6. **Show Results**: Display outputs using `agent_output`.
-
-## BEHAVIORAL GUIDELINES
-
-**Be Concise:**
-- Target 2-5 short lines maximum
-- Make every word count—no repetition or filler
-- Use lightweight structure for scannability (bullets, numbered lists, short prompts)
-- Avoid jargon (blocks, slugs, cron) unless the user asks
-
-**Be Proactive:**
-- Suggest next steps before being asked
-- Anticipate needs based on conversation context and user information
-- Look for opportunities to expand scope when relevant
-- Reveal capabilities through action, not explanation
-
-**Use Tools Effectively:**
-- Select the right tool for each task
-- **Always check `find_library_agent` before searching the marketplace**
-- Use `add_understanding` to capture valuable business context
-- When tool calls fail, try alternative approaches
-
-## CRITICAL REMINDER
-
-You are NOT a chatbot. You are NOT documentation. You are a partner who helps busy business owners get value quickly by showing proof through working automations. Bias toward action over explanation."""
-
-
-async def _build_system_prompt(
- user_id: str | None, has_conversation_history: bool = False
-) -> tuple[str, Any]:
- """Build the system prompt with user's business understanding context.
-
- Args:
- user_id: The user ID to fetch understanding for.
- has_conversation_history: Whether there's existing conversation history.
- If True, we don't tell the model to greet/introduce (since they're
- already in a conversation).
+ Returns the compressed prior messages (everything except the current message).
"""
- understanding = None
- if user_id:
- try:
- understanding = await get_business_understanding(user_id)
- except Exception as e:
- logger.warning(f"Failed to fetch business understanding: {e}")
+ prior = session.messages[:-1]
+ if len(prior) < 2:
+ return prior
- if understanding:
- context = format_understanding_for_prompt(understanding)
- elif has_conversation_history:
- # Don't tell model to greet if there's conversation history
- context = "No prior understanding saved yet. Continue the existing conversation naturally."
- else:
- context = "This is the first time you are meeting the user. Greet them and introduce them to the platform"
+ from backend.util.prompt import compress_context
- return DEFAULT_SYSTEM_PROMPT.replace("{users_information}", context), understanding
+ # Convert ChatMessages to dicts for compress_context
+ messages_dict = []
+ for msg in prior:
+ msg_dict: dict[str, Any] = {"role": msg.role}
+ if msg.content:
+ msg_dict["content"] = msg.content
+ if msg.tool_calls:
+ msg_dict["tool_calls"] = msg.tool_calls
+ if msg.tool_call_id:
+ msg_dict["tool_call_id"] = msg.tool_call_id
+ messages_dict.append(msg_dict)
-
-async def _generate_session_title(
- message: str,
- user_id: str | None = None,
- session_id: str | None = None,
-) -> str | None:
- """Generate a concise title for a chat session."""
- from backend.util.settings import Settings
-
- settings = Settings()
try:
- # Build extra_body for OpenRouter tracing
- extra_body: dict[str, Any] = {
- "posthogProperties": {"environment": settings.config.app_env.value},
- }
- if user_id:
- extra_body["user"] = user_id[:128]
- extra_body["posthogDistinctId"] = user_id
- if session_id:
- extra_body["session_id"] = session_id[:128]
+ import openai
- client = openai.AsyncOpenAI(api_key=config.api_key, base_url=config.base_url)
- response = await client.chat.completions.create(
- model=config.title_model,
- messages=[
- {
- "role": "system",
- "content": "Generate a very short title (3-6 words) for a chat conversation based on the user's first message. Return ONLY the title, no quotes or punctuation.",
- },
- {"role": "user", "content": message[:500]},
- ],
- max_tokens=20,
- extra_body=extra_body,
- )
- title = response.choices[0].message.content
- if title:
- title = title.strip().strip("\"'")
- return title[:47] + "..." if len(title) > 50 else title
- return None
+ async with openai.AsyncOpenAI(
+ api_key=config.api_key, base_url=config.base_url, timeout=30.0
+ ) as client:
+ result = await compress_context(
+ messages=messages_dict,
+ model=config.model,
+ client=client,
+ )
except Exception as e:
- logger.warning(f"Failed to generate session title: {e}")
+ logger.warning(f"[SDK] Context compression with LLM failed: {e}")
+ # Fall back to truncation-only (no LLM summarization)
+ result = await compress_context(
+ messages=messages_dict,
+ model=config.model,
+ client=None,
+ )
+
+ if result.was_compacted:
+ logger.info(
+ f"[SDK] Context compacted: {result.original_token_count} -> "
+ f"{result.token_count} tokens "
+ f"({result.messages_summarized} summarized, "
+ f"{result.messages_dropped} dropped)"
+ )
+ # Convert compressed dicts back to ChatMessages
+ return [
+ ChatMessage(
+ role=m["role"],
+ content=m.get("content"),
+ tool_calls=m.get("tool_calls"),
+ tool_call_id=m.get("tool_call_id"),
+ )
+ for m in result.messages
+ ]
+
+ return prior
+
+
+def _format_conversation_context(messages: list[ChatMessage]) -> str | None:
+ """Format conversation messages into a context prefix for the user message.
+
+ Returns a string like:
+
+ User: hello
+ You responded: Hi! How can I help?
+
+
+ Returns None if there are no messages to format.
+ """
+ if not messages:
return None
+ lines: list[str] = []
+ for msg in messages:
+ if not msg.content:
+ continue
+ if msg.role == "user":
+ lines.append(f"User: {msg.content}")
+ elif msg.role == "assistant":
+ lines.append(f"You responded: {msg.content}")
+ # Skip tool messages — they're internal details
+
+ if not lines:
+ return None
+
+ return "\n" + "\n".join(lines) + "\n"
+
async def stream_chat_completion_sdk(
session_id: str,
@@ -243,11 +205,10 @@ async def stream_chat_completion_sdk(
task = asyncio.create_task(
_update_title_async(session_id, first_message, user_id)
)
- # Store reference to prevent garbage collection
_background_tasks.add(task)
task.add_done_callback(_background_tasks.discard)
- # Check if there's conversation history (more than just the current message)
+ # Build system prompt (reuses non-SDK path with Langfuse support)
has_history = len(session.messages) > 1
system_prompt, _ = await _build_system_prompt(
user_id, has_conversation_history=has_history
@@ -260,35 +221,20 @@ async def stream_chat_completion_sdk(
yield StreamStart(messageId=message_id, taskId=task_id)
- # Track whether the stream completed normally via ResultMessage
stream_completed = False
try:
try:
from claude_agent_sdk import ClaudeAgentOptions, ClaudeSDKClient
- # Create MCP server with CoPilot tools
mcp_server = create_copilot_mcp_server()
- # For multi-turn conversations, write a session file so the CLI
- # loads full user+assistant context via --resume. This enables
- # turn-level compaction for long conversations.
- resume_id: str | None = None
- if len(session.messages) > 1:
- resume_id = write_session_file(session)
- if resume_id:
- logger.info(
- f"[SDK] Wrote session file for --resume: "
- f"{len(session.messages) - 1} prior messages"
- )
-
options = ClaudeAgentOptions(
system_prompt=system_prompt,
mcp_servers={"copilot": mcp_server}, # type: ignore[arg-type]
allowed_tools=COPILOT_TOOL_NAMES,
hooks=create_security_hooks(user_id), # type: ignore[arg-type]
cwd="/tmp",
- resume=resume_id,
)
adapter = SDKResponseAdapter(message_id=message_id)
@@ -296,14 +242,12 @@ async def stream_chat_completion_sdk(
try:
async with ClaudeSDKClient(options=options) as client:
- # Determine the current user message
current_message = message or ""
if not current_message and session.messages:
last_user = [m for m in session.messages if m.role == "user"]
if last_user:
current_message = last_user[-1].content or ""
- # Guard against empty messages
if not current_message.strip():
yield StreamError(
errorText="Message cannot be empty.",
@@ -312,38 +256,46 @@ async def stream_chat_completion_sdk(
yield StreamFinish()
return
- await client.query(current_message, session_id=session_id)
- logger.info(
- "[SDK] Query sent"
- + (" (with --resume)" if resume_id else " (new)")
- )
+ # Build query with conversation history context.
+ # Compress history first to handle long conversations.
+ query_message = current_message
+ if len(session.messages) > 1:
+ compressed = await _compress_conversation_history(session)
+ history_context = _format_conversation_context(compressed)
+ if history_context:
+ query_message = (
+ f"{history_context}\n\n"
+ f"Now, the user says:\n{current_message}"
+ )
+
+ logger.info(
+ f"[SDK] Sending query: {current_message[:80]!r}"
+ f" ({len(session.messages)} msgs in session)"
+ )
+ await client.query(query_message, session_id=session_id)
- # Track assistant response to save to session
- # We may need multiple assistant messages if text comes after tool results
assistant_response = ChatMessage(role="assistant", content="")
accumulated_tool_calls: list[dict[str, Any]] = []
has_appended_assistant = False
- has_tool_results = False # Track if we've received tool results
+ has_tool_results = False
- # Receive messages from the SDK
async for sdk_msg in client.receive_messages():
logger.debug(
- f"[SDK] Received: {type(sdk_msg).__name__} {getattr(sdk_msg, 'subtype', '')}"
+ f"[SDK] Received: {type(sdk_msg).__name__} "
+ f"{getattr(sdk_msg, 'subtype', '')}"
)
for response in adapter.convert_message(sdk_msg):
if isinstance(response, StreamStart):
continue
yield response
- # Accumulate text deltas into assistant response
if isinstance(response, StreamTextDelta):
delta = response.delta or ""
- # After tool results, create new assistant message for post-tool text
if has_tool_results and has_appended_assistant:
assistant_response = ChatMessage(
role="assistant", content=delta
)
- accumulated_tool_calls = [] # Reset for new message
+ accumulated_tool_calls = []
session.messages.append(assistant_response)
has_tool_results = False
else:
@@ -354,7 +306,6 @@ async def stream_chat_completion_sdk(
session.messages.append(assistant_response)
has_appended_assistant = True
- # Track tool calls on the assistant message
elif isinstance(response, StreamToolInputAvailable):
accumulated_tool_calls.append(
{
@@ -368,9 +319,7 @@ async def stream_chat_completion_sdk(
},
}
)
- # Update assistant message with tool calls
assistant_response.tool_calls = accumulated_tool_calls
- # Append assistant message if not already (tool-only response)
if not has_appended_assistant:
session.messages.append(assistant_response)
has_appended_assistant = True
@@ -392,23 +341,16 @@ async def stream_chat_completion_sdk(
elif isinstance(response, StreamFinish):
stream_completed = True
- # Break out of the message loop if we received finish signal
if stream_completed:
break
- # Ensure assistant response is saved even if no text deltas
- # (e.g., only tool calls were made)
if (
assistant_response.content or assistant_response.tool_calls
) and not has_appended_assistant:
session.messages.append(assistant_response)
finally:
- # Always clean up SDK tool-result files, even on error
_cleanup_sdk_tool_results()
- # Clean up session file written for --resume
- if resume_id:
- cleanup_session_file(resume_id)
except ImportError:
logger.warning(
@@ -421,24 +363,19 @@ async def stream_chat_completion_sdk(
stream_completed = True
yield response
- # Save the session with accumulated messages
await upsert_chat_session(session)
logger.debug(
f"[SDK] Session {session_id} saved with {len(session.messages)} messages"
)
- # Yield StreamFinish to signal completion to the caller (routes.py)
- # Only if one hasn't already been yielded by the stream
if not stream_completed:
yield StreamFinish()
except Exception as e:
logger.error(f"[SDK] Error: {e}", exc_info=True)
- # Save session even on error to preserve any partial response
try:
await upsert_chat_session(session)
except Exception as save_err:
logger.error(f"[SDK] Failed to save session on error: {save_err}")
- # Sanitize error message to avoid exposing internal details
yield StreamError(
errorText="An error occurred. Please try again.",
code="sdk_error",
diff --git a/autogpt_platform/backend/backend/api/features/chat/sdk/session_file.py b/autogpt_platform/backend/backend/api/features/chat/sdk/session_file.py
deleted file mode 100644
index ad88e35358..0000000000
--- a/autogpt_platform/backend/backend/api/features/chat/sdk/session_file.py
+++ /dev/null
@@ -1,119 +0,0 @@
-"""Session file management for Claude Code CLI --resume support.
-
-Writes conversation history as JSONL files to the CLI's session storage
-directory, enabling --resume to load full user+assistant context with
-turn-level compaction support.
-"""
-
-import json
-import logging
-import uuid
-from datetime import UTC, datetime
-from pathlib import Path
-
-from ..model import ChatSession
-
-logger = logging.getLogger(__name__)
-
-# The CLI stores sessions under ~/.claude/projects//.jsonl
-# The cwd path is encoded by replacing / with - and prefixing with -
-_CLAUDE_PROJECTS_DIR = Path.home() / ".claude" / "projects"
-
-
-def _encode_cwd(cwd: str) -> str:
- """Encode a working directory path for the CLI projects dir name."""
- return "-" + cwd.lstrip("/").replace("/", "-")
-
-
-def _get_project_dir(cwd: str) -> Path:
- """Get the CLI project directory for a given working directory.
-
- Resolves symlinks to match the CLI's behavior (e.g. /tmp -> /private/tmp
- on macOS).
- """
- resolved = str(Path(cwd).resolve())
- return _CLAUDE_PROJECTS_DIR / _encode_cwd(resolved)
-
-
-def write_session_file(
- session: ChatSession,
- cwd: str = "/tmp",
-) -> str | None:
- """Write a session's conversation history as a JSONL file for --resume.
-
- Returns the session ID to pass to --resume, or None if there's not enough
- history to warrant a file (< 2 messages).
- """
- # Only write if there's prior conversation (at least user + assistant)
- prior = [m for m in session.messages[:-1] if m.role in ("user", "assistant")]
- if len(prior) < 2:
- return None
-
- session_id = session.session_id
- resolved_cwd = str(Path(cwd).resolve())
- project_dir = _get_project_dir(cwd)
- project_dir.mkdir(parents=True, exist_ok=True)
-
- file_path = project_dir / f"{session_id}.jsonl"
- now = datetime.now(UTC).isoformat()
-
- lines: list[str] = []
- prev_uuid: str | None = None
-
- for msg in session.messages[:-1]:
- msg_uuid = str(uuid.uuid4())
-
- if msg.role == "user" and msg.content:
- line = {
- "parentUuid": prev_uuid,
- "isSidechain": False,
- "userType": "external",
- "cwd": resolved_cwd,
- "sessionId": session_id,
- "type": "user",
- "message": {"role": "user", "content": msg.content},
- "uuid": msg_uuid,
- "timestamp": now,
- }
- lines.append(json.dumps(line))
- prev_uuid = msg_uuid
-
- elif msg.role == "assistant" and msg.content:
- line = {
- "parentUuid": prev_uuid,
- "isSidechain": False,
- "userType": "external",
- "cwd": resolved_cwd,
- "sessionId": session_id,
- "type": "assistant",
- "message": {
- "role": "assistant",
- "content": [{"type": "text", "text": msg.content}],
- "model": "unknown",
- },
- "uuid": msg_uuid,
- "timestamp": now,
- }
- lines.append(json.dumps(line))
- prev_uuid = msg_uuid
-
- if not lines:
- return None
-
- try:
- file_path.write_text("\n".join(lines) + "\n")
- logger.debug(f"[SESSION] Wrote {len(lines)} messages to {file_path}")
- return session_id
- except OSError as e:
- logger.warning(f"[SESSION] Failed to write session file: {e}")
- return None
-
-
-def cleanup_session_file(session_id: str, cwd: str = "/tmp") -> None:
- """Remove a session file after use."""
- project_dir = _get_project_dir(cwd)
- file_path = project_dir / f"{session_id}.jsonl"
- try:
- file_path.unlink(missing_ok=True)
- except OSError:
- pass
diff --git a/autogpt_platform/backend/backend/api/features/chat/sdk/session_file_test.py b/autogpt_platform/backend/backend/api/features/chat/sdk/session_file_test.py
deleted file mode 100644
index 5c0eef1fb2..0000000000
--- a/autogpt_platform/backend/backend/api/features/chat/sdk/session_file_test.py
+++ /dev/null
@@ -1,222 +0,0 @@
-"""Unit tests for session file management."""
-
-import json
-from datetime import UTC, datetime
-from pathlib import Path
-from unittest.mock import patch
-
-from ..model import ChatMessage, ChatSession
-from .session_file import _get_project_dir, cleanup_session_file, write_session_file
-
-_NOW = datetime.now(UTC)
-
-
-def _make_session(
- messages: list[ChatMessage], session_id: str = "test-session"
-) -> ChatSession:
- return ChatSession(
- session_id=session_id,
- user_id="test-user",
- messages=messages,
- usage=[],
- started_at=_NOW,
- updated_at=_NOW,
- )
-
-
-# -- write_session_file ------------------------------------------------------
-
-
-def test_write_returns_none_for_short_history():
- """Sessions with < 2 prior messages shouldn't generate a file."""
- session = _make_session(
- [
- ChatMessage(role="user", content="hello"),
- ]
- )
- assert write_session_file(session) is None
-
-
-def test_write_returns_none_for_single_pair():
- """A single user message (the current one) with no prior history."""
- session = _make_session(
- [
- ChatMessage(role="user", content="current message"),
- ]
- )
- assert write_session_file(session) is None
-
-
-def test_write_creates_valid_jsonl(tmp_path: Path):
- """Multi-turn session should produce valid JSONL with correct structure."""
- session = _make_session(
- [
- ChatMessage(role="user", content="hello"),
- ChatMessage(role="assistant", content="Hi there!"),
- ChatMessage(role="user", content="how are you"), # current message
- ],
- session_id="sess-123",
- )
-
- with patch(
- "backend.api.features.chat.sdk.session_file._get_project_dir",
- return_value=tmp_path,
- ):
- result = write_session_file(session)
-
- assert result == "sess-123"
-
- # Verify the file exists and is valid JSONL
- file_path = tmp_path / "sess-123.jsonl"
- assert file_path.exists()
-
- lines = file_path.read_text().strip().split("\n")
- # Should have 2 lines (prior messages only, not the current/last one)
- assert len(lines) == 2
-
- # Verify first line (user message)
- line1 = json.loads(lines[0])
- assert line1["type"] == "user"
- assert line1["message"]["role"] == "user"
- assert line1["message"]["content"] == "hello"
- assert line1["sessionId"] == "sess-123"
- assert line1["parentUuid"] is None # First message has no parent
- assert "uuid" in line1
- assert "timestamp" in line1
-
- # Verify second line (assistant message)
- line2 = json.loads(lines[1])
- assert line2["type"] == "assistant"
- assert line2["message"]["role"] == "assistant"
- assert line2["message"]["content"] == [{"type": "text", "text": "Hi there!"}]
- assert line2["parentUuid"] == line1["uuid"] # Chained to previous
-
-
-def test_write_skips_tool_messages(tmp_path: Path):
- """Tool messages should be skipped in the session file."""
- session = _make_session(
- [
- ChatMessage(role="user", content="find agents"),
- ChatMessage(role="assistant", content="Let me search."),
- ChatMessage(role="tool", content="found 3", tool_call_id="tc1"),
- ChatMessage(role="assistant", content="I found 3 agents."),
- ChatMessage(role="user", content="run the first one"),
- ],
- session_id="sess-tools",
- )
-
- with patch(
- "backend.api.features.chat.sdk.session_file._get_project_dir",
- return_value=tmp_path,
- ):
- result = write_session_file(session)
-
- assert result == "sess-tools"
- file_path = tmp_path / "sess-tools.jsonl"
- lines = file_path.read_text().strip().split("\n")
-
- # Should have 3 lines: user, assistant, assistant (tool message skipped,
- # last user message excluded as current)
- assert len(lines) == 3
- types = [json.loads(line)["type"] for line in lines]
- assert types == ["user", "assistant", "assistant"]
-
-
-def test_write_skips_empty_content(tmp_path: Path):
- """Messages with empty content should be skipped."""
- session = _make_session(
- [
- ChatMessage(role="user", content="hello"),
- ChatMessage(role="assistant", content=""),
- ChatMessage(role="assistant", content="real response"),
- ChatMessage(role="user", content="next"),
- ],
- session_id="sess-empty",
- )
-
- with patch(
- "backend.api.features.chat.sdk.session_file._get_project_dir",
- return_value=tmp_path,
- ):
- result = write_session_file(session)
-
- assert result == "sess-empty"
- file_path = tmp_path / "sess-empty.jsonl"
- lines = file_path.read_text().strip().split("\n")
- # user + assistant (non-empty) = 2 lines
- assert len(lines) == 2
-
-
-# -- cleanup_session_file ----------------------------------------------------
-
-
-def test_cleanup_removes_file(tmp_path: Path):
- """cleanup_session_file should remove the session file."""
- file_path = tmp_path / "sess-cleanup.jsonl"
- file_path.write_text("{}\n")
- assert file_path.exists()
-
- with patch(
- "backend.api.features.chat.sdk.session_file._get_project_dir",
- return_value=tmp_path,
- ):
- cleanup_session_file("sess-cleanup")
-
- assert not file_path.exists()
-
-
-def test_cleanup_no_error_if_missing(tmp_path: Path):
- """cleanup_session_file should not raise if file doesn't exist."""
- with patch(
- "backend.api.features.chat.sdk.session_file._get_project_dir",
- return_value=tmp_path,
- ):
- cleanup_session_file("nonexistent") # Should not raise
-
-
-# -- _get_project_dir --------------------------------------------------------
-
-
-def test_get_project_dir_resolves_symlinks(tmp_path: Path):
- """_get_project_dir should resolve symlinks so the path matches the CLI."""
- # Create a symlink: tmp_path/link -> tmp_path/real
- real_dir = tmp_path / "real"
- real_dir.mkdir()
- link = tmp_path / "link"
- link.symlink_to(real_dir)
-
- with patch(
- "backend.api.features.chat.sdk.session_file._CLAUDE_PROJECTS_DIR",
- tmp_path / "projects",
- ):
- result = _get_project_dir(str(link))
-
- # Should resolve the symlink and encode the real path
- expected_encoded = "-" + str(real_dir).lstrip("/").replace("/", "-")
- assert result.name == expected_encoded
-
-
-def test_write_uses_resolved_cwd_in_messages(tmp_path: Path):
- """The cwd field in JSONL messages should use the resolved path."""
- session = _make_session(
- [
- ChatMessage(role="user", content="hello"),
- ChatMessage(role="assistant", content="Hi!"),
- ChatMessage(role="user", content="current"),
- ],
- session_id="sess-cwd",
- )
-
- with patch(
- "backend.api.features.chat.sdk.session_file._get_project_dir",
- return_value=tmp_path,
- ):
- write_session_file(session, cwd="/tmp")
-
- file_path = tmp_path / "sess-cwd.jsonl"
- lines = file_path.read_text().strip().split("\n")
- for line in lines:
- obj = json.loads(line)
- # On macOS /tmp resolves to /private/tmp; on Linux stays /tmp
- resolved = str(Path("/tmp").resolve())
- assert obj["cwd"] == resolved
diff --git a/autogpt_platform/backend/backend/api/features/chat/service.py b/autogpt_platform/backend/backend/api/features/chat/service.py
index 2514f826a9..6616537d18 100644
--- a/autogpt_platform/backend/backend/api/features/chat/service.py
+++ b/autogpt_platform/backend/backend/api/features/chat/service.py
@@ -245,12 +245,16 @@ async def _get_system_prompt_template(context: str) -> str:
return DEFAULT_SYSTEM_PROMPT.format(users_information=context)
-async def _build_system_prompt(user_id: str | None) -> tuple[str, Any]:
+async def _build_system_prompt(
+ user_id: str | None, has_conversation_history: bool = False
+) -> tuple[str, Any]:
"""Build the full system prompt including business understanding if available.
Args:
- user_id: The user ID for fetching business understanding
- If "default" and this is the user's first session, will use "onboarding" instead.
+ user_id: The user ID for fetching business understanding.
+ has_conversation_history: Whether there's existing conversation history.
+ If True, we don't tell the model to greet/introduce (since they're
+ already in a conversation).
Returns:
Tuple of (compiled prompt string, business understanding object)
@@ -266,6 +270,8 @@ async def _build_system_prompt(user_id: str | None) -> tuple[str, Any]:
if understanding:
context = format_understanding_for_prompt(understanding)
+ elif has_conversation_history:
+ context = "No prior understanding saved yet. Continue the existing conversation naturally."
else:
context = "This is the first time you are meeting the user. Greet them and introduce them to the platform"
@@ -1229,7 +1235,7 @@ async def _stream_chat_chunks(
total_time = (time_module.perf_counter() - stream_chunks_start) * 1000
logger.info(
- f"[TIMING] _stream_chat_chunks COMPLETED in {total_time/1000:.1f}s; "
+ f"[TIMING] _stream_chat_chunks COMPLETED in {total_time / 1000:.1f}s; "
f"session={session.session_id}, user={session.user_id}",
extra={"json_fields": {**log_meta, "total_time_ms": total_time}},
)