Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions autobot-backend/api/anthropic_compat.py
Original file line number Diff line number Diff line change
Expand Up @@ -321,6 +321,8 @@ async def messages(
)

# Non-streaming path — use fallback coordinator for quota-triggered model switch
from llm_shared.model_fallback_coordinator import get_fallback_coordinator

llm_response = await get_fallback_coordinator().execute_with_fallback(llm_request, registry)
if llm_response.error:
raise HTTPException(status_code=502, detail=llm_response.error)
Expand Down
87 changes: 87 additions & 0 deletions autobot-backend/api/chat.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@
ChatPreferences,
ChatSaveData,
ChatStatsData,
ConversationSummarizeData,
ConversationSummarizeRequest,
DetectLanguageData,
DetectLanguageRequest,
EnhancedChatCapabilitiesData,
Expand Down Expand Up @@ -2187,3 +2189,88 @@ async def detect_language(
)
result = await agent.handle_detect_language(req)
return JSONResponse(content=result, media_type="application/json; charset=utf-8") # codeql[py/stack-trace-exposure]


@router.post("/chat/summarize", response_model=DataResponse[Dict[str, Any]])
@with_error_handling(
category=ErrorCategory.SERVER_ERROR,
operation="summarize_conversation",
error_code_prefix="CHAT_SUMMARIZE",
)
async def summarize_conversation(
body: "ConversationSummarizeRequest",
request: Request,
current_user: dict = Depends(get_current_user),
):
"""
Summarize a range of conversation messages for context overflow protection (GH#9043).

When a conversation approaches the model's context window limit, this endpoint
generates a compact summary of the earliest messages to enable a rolling window
approach without losing essential context.
"""
from api.schemas_chat import ConversationSummarizeData, ConversationSummarizeRequest

request_id = generate_request_id()
logger.info("[%s] Summarizing %d messages from session %s", request_id, len(body.message_ids), body.session_id)

# Validate session ownership
await validate_chat_ownership(body.session_id, request)

# Get chat history manager and retrieve messages
chat_history = get_chat_history_manager()
all_messages = await chat_history.get_messages(body.session_id)

# Filter to requested message IDs
messages_to_summarize = [msg for msg in all_messages if msg.get("message_id") in body.message_ids]

if not messages_to_summarize:
raise HTTPException(status_code=404, detail="No messages found with the provided IDs")

# Build prompt for summarization
conversation_text = "\n\n".join(
[f"{msg.get('role', 'unknown').upper()}: {msg.get('content', '')}" for msg in messages_to_summarize]
)

summarization_prompt = f"""Summarize the following conversation segment concisely. Preserve key context, decisions, and technical details that would be needed to continue the conversation naturally.

Target length: approximately {body.target_length or 500} tokens.

Conversation:
{conversation_text}

Summary:"""

# Get LLM service and generate summary
llm_service = get_llm_service(request)

try:
summary_response = await llm_service.generate_response(
messages=[{"role": "user", "content": summarization_prompt}],
model="gpt-4o-mini", # Fast, cheap model for summarization
max_tokens=body.target_length or 500,
temperature=0.3, # Lower temperature for focused summarization
)

summary_text = summary_response.get("content", "")
summary_tokens = summary_response.get("usage", {}).get("total_tokens", 0)

except Exception as e:
logger.exception("[%s] Failed to generate summary", request_id)
raise HTTPException(status_code=500, detail=f"Summarization failed: {str(e)}")

result_data = ConversationSummarizeData(
summary=summary_text,
original_message_count=len(messages_to_summarize),
summary_token_count=summary_tokens,
timestamp=utc_timestamp(),
)

logger.info(
"[%s] Generated summary: %d messages → %d tokens",
request_id,
result_data.original_message_count,
result_data.summary_token_count or 0,
)

return DataResponse(data=result_data.model_dump())
2 changes: 2 additions & 0 deletions autobot-backend/api/openai_compat.py
Original file line number Diff line number Diff line change
Expand Up @@ -381,6 +381,8 @@ async def chat_completions(
)

# Non-streaming path — use fallback coordinator for quota-triggered model switch
from llm_shared.model_fallback_coordinator import get_fallback_coordinator

llm_response = await get_fallback_coordinator().execute_with_fallback(llm_request, registry)
if llm_response.error:
raise HTTPException(status_code=502, detail=llm_response.error)
Expand Down
3 changes: 3 additions & 0 deletions autobot-backend/api/schemas_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ class AgentCommandExecuteResponse(BaseModel):
output: str | None = None
status: str | None = None


class AgentHealthResponse(BaseModel):
"""Response for GET /health/enhanced."""

Expand All @@ -65,6 +66,7 @@ class MemoryStatisticsResponse(BaseModel):
active_tasks: Any | None = None
performance_insights: Any | None = None


class MemoryTaskHistoryResponse(BaseModel):
"""Response for GET /tasks/history."""

Expand Down Expand Up @@ -123,6 +125,7 @@ class MemoryDocumentReferencesResponse(BaseModel):
timestamp: str
references: Any | None = None


class MemoryEmbeddingCacheStatsResponse(BaseModel):
"""Response for GET /embeddings/cache-stats."""

Expand Down
22 changes: 22 additions & 0 deletions autobot-backend/api/schemas_chat.py
Original file line number Diff line number Diff line change
Expand Up @@ -669,3 +669,25 @@ class SessionFolderAssign(BaseModel):
"""Request body for PUT /chat/sessions/{session_id}/folder."""

folder_id: str | None = Field(None, description="Folder ID to assign; None removes from folder")


# ── Context Overflow Protection schemas (GH#9043) ────────────────────────────


class ConversationSummarizeRequest(BaseModel):
"""Request body for POST /chat/summarize."""

session_id: str = Field(..., description="Chat session ID")
message_ids: List[str] = Field(..., min_length=1, description="Message IDs to summarize")
target_length: int | None = Field(
500, ge=100, le=2000, description="Target summary length in tokens (default: 500)"
)


class ConversationSummarizeData(BaseModel):
"""data payload for POST /chat/summarize."""

summary: str = Field(..., description="Generated summary of the conversation segment")
original_message_count: int = Field(..., description="Number of messages summarized")
summary_token_count: int | None = Field(None, description="Token count of the summary")
timestamp: str = Field(..., description="ISO timestamp when summary was generated")
7 changes: 7 additions & 0 deletions autobot-backend/api/schemas_code.py
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,7 @@ class LogRecentResponse(BaseModel):
source: str
error: str | None = None


class LogReadResponse(BaseModel):
"""Response for GET /logs/read/{filename}."""

Expand Down Expand Up @@ -282,6 +283,7 @@ class GitMCPOperationResponse(BaseModel):
timestamp: str
errors: str | None = None


class GitMCPInfoResponse(BaseModel):
"""Response for GET /git/mcp/info."""

Expand Down Expand Up @@ -351,6 +353,7 @@ class SkillsGapResponse(BaseModel):
name: str | None = None
tools_found: List[str] | None = None


class SkillsDraftListItem(BaseModel):
"""Single draft entry in the list response."""

Expand All @@ -361,6 +364,7 @@ class SkillsDraftListItem(BaseModel):
created_at: Any | None = None
trust_level: Any | None = None


class SkillsDraftTestResponse(BaseModel):
"""Response for POST /skills-governance/drafts/{skill_id}/test."""

Expand Down Expand Up @@ -536,18 +540,21 @@ class AccessControlMetricsResponse(BaseModel):
success: bool
data: Dict[str, Any] | None = None


class AccessControlEndpointMetricsResponse(BaseModel):
"""Response for GET /access-control/endpoint/{endpoint:path}."""

success: bool
data: Any | None = None


class AccessControlUserMetricsResponse(BaseModel):
"""Response for GET /access-control/user/{username}."""

success: bool
data: Any | None = None


class AccessControlCleanupResponse(SuccessMessageResponse):
"""Response for POST /access-control/cleanup."""

Expand Down
1 change: 1 addition & 0 deletions autobot-backend/api/schemas_common.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ class SuccessDataResponse(BaseModel):
message: str
data: Dict[str, Any] | None = None


class SuccessResponse(BaseModel):
"""Minimal success/failure envelope used by several endpoints."""

Expand Down
12 changes: 12 additions & 0 deletions autobot-backend/api/schemas_terminal.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ class TerminalSessionCreateResponse(BaseModel):
created_at: str
ssh_keys: Dict[str, Any] | None = None


class TerminalSessionItem(BaseModel):
"""Single session entry within a list response."""

Expand Down Expand Up @@ -72,6 +73,7 @@ class SSHKeyItem(BaseModel):
has_passphrase: bool | None = None
key_path: str | None = None


class SSHKeyListResponse(BaseModel):
"""Response for GET /sessions/{session_id}/ssh-keys."""

Expand Down Expand Up @@ -143,6 +145,7 @@ class TerminalCommandHistoryResponse(BaseModel):
total_commands: int | None = None
message: str | None = None


class TerminalAuditLogResponse(BaseModel):
"""Response for GET /audit/{session_id}."""

Expand Down Expand Up @@ -190,6 +193,7 @@ class TerminalHealthResponse(BaseModel):
metrics: TerminalHealthMetrics | None = None
error: str | None = None


class TerminalStatusFeatures(BaseModel):
pty_support: bool
websocket_support: bool
Expand All @@ -203,6 +207,7 @@ class TerminalStatusSessionInfo(BaseModel):
active_sessions: int
max_concurrent_sessions: int | None = None


class TerminalSystemStatusResponse(BaseModel):
"""Response for GET /status."""

Expand All @@ -212,6 +217,7 @@ class TerminalSystemStatusResponse(BaseModel):
session_info: TerminalStatusSessionInfo | None = None
error: str | None = None


class TerminalCapabilitiesResponse(BaseModel):
"""Response for GET /capabilities."""

Expand Down Expand Up @@ -244,6 +250,7 @@ class TerminalImplementationItem(BaseModel):
approval_workflow: bool
service_layer: str | None = None


class TerminalFeaturesResponse(BaseModel):
"""Response for GET /features."""

Expand Down Expand Up @@ -315,6 +322,7 @@ class AgentTerminalSessionCreateResponse(BaseModel):
created_at: float
pty_session_id: str | None = None


class AgentTerminalSessionItem(BaseModel):
"""Single session entry within list response."""

Expand All @@ -329,6 +337,7 @@ class AgentTerminalSessionItem(BaseModel):
command_count: int
pty_session_id: str | None = None


class AgentTerminalSessionListResponse(BaseModel):
"""Response for GET /agent-terminal/sessions."""

Expand Down Expand Up @@ -376,6 +385,7 @@ class AgentTerminalCommandStateResponse(BaseModel):
approved_by_user_id: str | None = None
approval_comment: str | None = None


class AgentTerminalInfoResponse(BaseModel):
"""Response for GET /agent-terminal/."""

Expand Down Expand Up @@ -416,6 +426,7 @@ class AgentTerminalHostSelectionGetResponse(BaseModel):
created_at: str
updated_at: str | None = None


class AgentTerminalHostSelectionSubmitResponse(BaseModel):
"""Response for POST /agent-terminal/host-selection/{request_id}/select."""

Expand All @@ -425,6 +436,7 @@ class AgentTerminalHostSelectionSubmitResponse(BaseModel):
selected_host_name: str | None = None
connection_info: Dict[str, Any] | None = None


class AgentTerminalHostSelectionCancelResponse(BaseModel):
"""Response for POST /agent-terminal/host-selection/{request_id}/cancel."""

Expand Down
Loading
Loading