mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-08-17 18:25:42 +02:00
[aidan] fix/chat-history: stop retaining unopened histories
This commit is contained in:
@@ -1,15 +1,16 @@
|
||||
from backend.config.Apps import SubApp
|
||||
from backend.apps.agents.agent_manager import agent_manager
|
||||
from backend.apps.agents.core.ws_manager import ws_manager
|
||||
from backend.apps.agents.core.models import AgentConfig, ApprovalResponse
|
||||
from backend.apps.agents.manager.session.history_compaction import estimate_post_compact_input
|
||||
from contextlib import asynccontextmanager
|
||||
from fastapi import WebSocket, WebSocketDisconnect, HTTPException
|
||||
from fastapi.responses import JSONResponse
|
||||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
import time
|
||||
from contextlib import asynccontextmanager
|
||||
from typing import Any, Dict
|
||||
|
||||
from fastapi import HTTPException
|
||||
from typeguard import typechecked
|
||||
|
||||
from backend.apps.agents.agent_manager import agent_manager
|
||||
from backend.apps.agents.core.models import AgentConfig, AgentSession, ApprovalResponse
|
||||
from backend.apps.agents.manager.session.history_compaction import estimate_post_compact_input
|
||||
from backend.config.Apps import SubApp
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -39,10 +40,31 @@ async def agents_lifespan():
|
||||
agents = SubApp("agents", agents_lifespan)
|
||||
|
||||
|
||||
@typechecked
|
||||
def p_session_list_item(session: AgentSession) -> Dict[str, Any]:
|
||||
"""Serialize dashboard metadata without retaining the full chat history."""
|
||||
data = session.model_dump(mode="json", exclude={"messages"})
|
||||
messages = session.messages
|
||||
last_content = messages[-1].content if messages else ""
|
||||
first_user_content = next(
|
||||
(message.content for message in messages if message.role == "user"),
|
||||
"",
|
||||
)
|
||||
data.update(
|
||||
messages=[],
|
||||
last_message_preview=last_content[:120] if isinstance(last_content, str) else "",
|
||||
first_user_message=(
|
||||
first_user_content[:200] if isinstance(first_user_content, str) else ""
|
||||
),
|
||||
message_count=len(messages),
|
||||
)
|
||||
return data
|
||||
|
||||
|
||||
@agents.router.get("/sessions")
|
||||
async def list_sessions(dashboard_id: str = ""):
|
||||
sessions = agent_manager.get_all_sessions(dashboard_id=dashboard_id or None)
|
||||
return {"sessions": [s.model_dump(mode="json") for s in sessions]}
|
||||
return {"sessions": [p_session_list_item(s) for s in sessions]}
|
||||
|
||||
@agents.router.get("/activity")
|
||||
async def agent_activity():
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
import asyncio
|
||||
|
||||
from pytest import MonkeyPatch
|
||||
|
||||
from backend.apps.agents import agents as agents_module
|
||||
from backend.apps.agents.core.models import AgentSession, Message
|
||||
|
||||
|
||||
def test_session_list_item_replaces_messages_with_compact_metadata(
|
||||
monkeypatch: MonkeyPatch,
|
||||
) -> None:
|
||||
first_prompt = "p" * 250
|
||||
last_reply = "r" * 150
|
||||
session = AgentSession(
|
||||
name="Test session",
|
||||
messages=[
|
||||
Message(role="system", content="system"),
|
||||
Message(role="user", content=first_prompt),
|
||||
Message(role="assistant", content=last_reply),
|
||||
],
|
||||
)
|
||||
|
||||
monkeypatch.setattr(
|
||||
agents_module.agent_manager,
|
||||
"get_all_sessions",
|
||||
lambda dashboard_id=None: [session],
|
||||
)
|
||||
item = asyncio.run(agents_module.list_sessions())["sessions"][0]
|
||||
|
||||
assert item["messages"] == []
|
||||
assert item["message_count"] == 3
|
||||
assert item["first_user_message"] == first_prompt[:200]
|
||||
assert item["last_message_preview"] == last_reply[:120]
|
||||
|
||||
|
||||
def test_session_list_item_handles_empty_and_non_text_content(
|
||||
monkeypatch: MonkeyPatch,
|
||||
) -> None:
|
||||
sessions = [
|
||||
AgentSession(name="Empty"),
|
||||
AgentSession(
|
||||
name="Images",
|
||||
messages=[Message(role="user", content=[{"type": "image"}])],
|
||||
),
|
||||
]
|
||||
monkeypatch.setattr(
|
||||
agents_module.agent_manager,
|
||||
"get_all_sessions",
|
||||
lambda dashboard_id=None: sessions,
|
||||
)
|
||||
empty, non_text = asyncio.run(agents_module.list_sessions())["sessions"]
|
||||
|
||||
assert empty["messages"] == []
|
||||
assert empty["message_count"] == 0
|
||||
assert empty["first_user_message"] == ""
|
||||
assert empty["last_message_preview"] == ""
|
||||
assert non_text["message_count"] == 1
|
||||
assert non_text["first_user_message"] == ""
|
||||
assert non_text["last_message_preview"] == ""
|
||||
@@ -206,20 +206,24 @@ const BrowserAgentInlineFeed: React.FC<Props> = ({ parentSessionId, browserId })
|
||||
}
|
||||
}, [browserSessions.length, parentSessionId, dispatch]);
|
||||
|
||||
const sessionsWithEntries = useMemo(() => {
|
||||
const sessionsWithHistoricalEntries = useMemo(() => {
|
||||
return browserSessions.map((session) => {
|
||||
const entries: FeedEntry[] = [];
|
||||
for (const msg of session.messages) {
|
||||
const entry = formatMessage(msg);
|
||||
if (entry) entries.push(entry);
|
||||
}
|
||||
const stream: StreamingMessage | undefined = streamingBySession[session.id];
|
||||
if (stream?.role === 'assistant' && stream.content) {
|
||||
entries.push({ type: 'thought', text: stream.content });
|
||||
}
|
||||
return { session, entries };
|
||||
});
|
||||
}, [browserSessions, streamingBySession]);
|
||||
}, [browserSessions]);
|
||||
|
||||
const sessionsWithEntries = sessionsWithHistoricalEntries.map(({ session, entries }) => {
|
||||
const stream: StreamingMessage | undefined = streamingBySession[session.id];
|
||||
if (stream?.role === 'assistant' && stream.content) {
|
||||
return { session, entries: [...entries, { type: 'thought' as const, text: stream.content }] };
|
||||
}
|
||||
return { session, entries };
|
||||
});
|
||||
|
||||
const totalMessages = browserSessions.reduce(
|
||||
(n, s) => n + s.messages.length + (streamingBySession[s.id] ? 1 : 0),
|
||||
|
||||
@@ -339,15 +339,20 @@ const AgentCard: React.FC<Props> = ({
|
||||
return Boolean(sourceWorkflow);
|
||||
}, [workflowRunsMap, sourceWorkflow, session.id, session.workflow_test_state]);
|
||||
const hasUserPrompt = useMemo(
|
||||
() => (session.messages || []).some((m) => m.role === 'user' && !m.hidden),
|
||||
[session.messages],
|
||||
() => session.messages.length > 0
|
||||
? session.messages.some((m) => m.role === 'user' && !m.hidden)
|
||||
: !!session.first_user_message,
|
||||
[session.messages, session.first_user_message],
|
||||
);
|
||||
const messageCount = session.messages.length > 0
|
||||
? session.messages.length
|
||||
: session.message_count ?? 0;
|
||||
const isConvertBlockedByTurn = session.status !== 'completed' && session.status !== 'stopped';
|
||||
const showConvertToWorkflow =
|
||||
!session.is_welcome_draft &&
|
||||
!isWorkflowRunnerSession &&
|
||||
hasUserPrompt &&
|
||||
(session.messages.length >= 2 || isConvertBlockedByTurn || !!workflowSuggestion);
|
||||
(messageCount >= 2 || isConvertBlockedByTurn || !!workflowSuggestion);
|
||||
const canConvertToWorkflow = showConvertToWorkflow && !isConvertBlockedByTurn;
|
||||
// Curated picker label with a tidy fallback for unknowns.
|
||||
const friendlyModelLabel = useMemo(() => {
|
||||
@@ -653,7 +658,7 @@ const AgentCard: React.FC<Props> = ({
|
||||
).slice(0, 120)
|
||||
: lastMessage && typeof lastMessage.content === 'string'
|
||||
? lastMessage.content.slice(0, 120)
|
||||
: '';
|
||||
: session.last_message_preview ?? '';
|
||||
const hasPending = session.pending_approvals.length > 0;
|
||||
const pendingReq = session.pending_approvals[0];
|
||||
|
||||
|
||||
@@ -366,7 +366,10 @@ export function useDashboardLifecycle({
|
||||
if (!dash) return;
|
||||
if (!dash.auto_named && dash.name !== 'Untitled Dashboard') return;
|
||||
const hasUserMessage = Object.values(sessions).some(
|
||||
(s) => s.dashboard_id === dashboardId && s.messages?.some((m) => m.role === 'user'),
|
||||
(s) => s.dashboard_id === dashboardId && (
|
||||
s.messages?.some((m) => m.role === 'user') ||
|
||||
(s.messages.length === 0 && !!s.first_user_message)
|
||||
),
|
||||
);
|
||||
if (!hasUserMessage) return;
|
||||
namedOnFirstMessageRef.current = dashboardId;
|
||||
|
||||
@@ -84,6 +84,10 @@ export interface AgentSession {
|
||||
cost_usd: number;
|
||||
tokens: { input: number; output: number };
|
||||
messages: AgentMessage[];
|
||||
/** Compact dashboard-list metadata; full messages are fetched when a chat opens. */
|
||||
last_message_preview?: string;
|
||||
first_user_message?: string;
|
||||
message_count?: number;
|
||||
pending_approvals: ApprovalRequest[];
|
||||
branches: Record<string, MessageBranch>;
|
||||
active_branch_id: string;
|
||||
|
||||
@@ -36,8 +36,13 @@ export function displayChatTitle(session: AgentSession | null | undefined): stri
|
||||
return session.name;
|
||||
}
|
||||
const firstUserMsg = session.messages?.find((m) => m.role === 'user');
|
||||
if (firstUserMsg && typeof firstUserMsg.content === 'string') {
|
||||
const truncated = truncateForTitle(firstUserMsg.content);
|
||||
const firstUserContent = firstUserMsg && typeof firstUserMsg.content === 'string'
|
||||
? firstUserMsg.content
|
||||
: session.messages.length === 0
|
||||
? session.first_user_message
|
||||
: undefined;
|
||||
if (firstUserContent) {
|
||||
const truncated = truncateForTitle(firstUserContent);
|
||||
if (truncated) return truncated;
|
||||
}
|
||||
return session.mode === 'view-builder' ? 'Untitled App' : SESSION_NAME_PLACEHOLDER;
|
||||
|
||||
Reference in New Issue
Block a user