Skip to content
Open
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
71 changes: 70 additions & 1 deletion backend/app/api/chat_sessions.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from datetime import datetime, timezone as tz
from typing import Optional

from fastapi import APIRouter, Depends, HTTPException, Query, status
from fastapi import APIRouter, Depends, HTTPException, Query
from pydantic import BaseModel
from sqlalchemy import select, func
from sqlalchemy.ext.asyncio import AsyncSession
Expand All @@ -17,6 +17,7 @@
from app.models.user import User

router = APIRouter(prefix="/api/agents", tags=["chat-sessions"])
DEFAULT_CONTEXT_BUDGET_TOKENS = 128000


def _is_admin_or_creator(user: User, agent: Agent) -> bool:
Expand Down Expand Up @@ -56,6 +57,14 @@ class PatchSessionIn(BaseModel):
title: str


class ContextBudgetOut(BaseModel):
window_size_messages: int
session_messages_total: int
estimated_tokens_current_window: int
budget_tokens: int
usage_ratio: float


@router.get("/{agent_id}/sessions")
async def list_sessions(
agent_id: uuid.UUID,
Expand Down Expand Up @@ -279,6 +288,66 @@ async def delete_session(
return None


@router.get("/{agent_id}/sessions/{session_id}/context-budget", response_model=ContextBudgetOut)
async def get_session_context_budget(
agent_id: uuid.UUID,
session_id: uuid.UUID,
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
"""Return a lightweight context-budget snapshot for one chat session."""
# Allow looking up sessions where agent_id OR peer_agent_id matches
result = await db.execute(
select(ChatSession).where(
ChatSession.id == session_id,
(ChatSession.agent_id == agent_id) | (ChatSession.peer_agent_id == agent_id),
)
)
session = result.scalar_one_or_none()
if not session:
raise HTTPException(status_code=404, detail="Session not found")

# Permission: owner, admin, or creator can view
agent_result = await db.execute(select(Agent).where(Agent.id == agent_id))
agent = agent_result.scalar_one_or_none()
if str(session.user_id) != str(current_user.id) and not _is_admin_or_creator(current_user, agent):
raise HTTPException(status_code=403, detail="Not authorized to view this session")

window_size = max(getattr(agent, "context_window_size", 100) or 100, 1)

total_result = await db.execute(
select(func.count(ChatMessage.id)).where(ChatMessage.conversation_id == str(session_id))
)
session_messages_total = int(total_result.scalar() or 0)

window_result = await db.execute(
select(ChatMessage.content)
.where(ChatMessage.conversation_id == str(session_id))
.order_by(ChatMessage.created_at.desc())
.limit(window_size)
)
window_contents = window_result.scalars().all()

total_chars = sum(len(content or "") for content in window_contents)
# Reuse existing char-based estimator for consistency across code paths.
from app.services.token_tracker import estimate_tokens_from_chars
estimated_tokens_current_window = estimate_tokens_from_chars(total_chars) if total_chars > 0 else 0

budget_tokens = DEFAULT_CONTEXT_BUDGET_TOKENS
usage_ratio = round(
(estimated_tokens_current_window / budget_tokens) if budget_tokens > 0 else 0.0,
4,
)

return ContextBudgetOut(
window_size_messages=window_size,
session_messages_total=session_messages_total,
estimated_tokens_current_window=estimated_tokens_current_window,
budget_tokens=budget_tokens,
usage_ratio=usage_ratio,
)


@router.get("/{agent_id}/sessions/{session_id}/messages")
async def get_session_messages(
agent_id: uuid.UUID,
Expand Down
15 changes: 13 additions & 2 deletions frontend/src/i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -379,7 +379,18 @@
"attachment": "Attachment",
"uploadFailed": "Upload failed",
"askAboutFile": "Ask about {{name}}...",
"thinking": "Thinking..."
"thinking": "Thinking...",
"contextBudget": {
"aria": "View context window info",
"title": "Context Window Info",
"windowSize": "Window messages",
"sessionMessages": "Session total messages",
"estimatedTokens": "Estimated window tokens",
"budgetTokens": "Budget tokens",
"usageRatio": "Usage ratio",
"loading": "Loading...",
"unavailable": "No session data yet"
}
},
"activityLog": {
"title": "Activity Log",
Expand Down Expand Up @@ -1037,4 +1048,4 @@
"ws_note": "WebSocket mode requires no public IP, callback URL, or domain verification (ICP). The connection is managed automatically."
}
}
}
}
13 changes: 12 additions & 1 deletion frontend/src/i18n/zh.json
Original file line number Diff line number Diff line change
Expand Up @@ -387,7 +387,18 @@
"attachment": "附件",
"uploadFailed": "上传失败",
"askAboutFile": "关于 {{name}} 的问题...",
"thinking": "思考中..."
"thinking": "思考中...",
"contextBudget": {
"aria": "查看上下文窗口信息",
"title": "上下文窗口信息",
"windowSize": "窗口消息条数",
"sessionMessages": "会话总消息条数",
"estimatedTokens": "窗口估算 Token",
"budgetTokens": "预算 Token",
"usageRatio": "使用率",
"loading": "加载中...",
"unavailable": "暂无会话数据"
}
},
"activityLog": {
"title": "工作日志",
Expand Down
Loading