mirror of
https://github.com/furyhawk/agent_alpha.git
synced 2026-07-21 02:25:34 +00:00
feat(session): add title management for sessions in backend and frontend
This commit is contained in:
@@ -361,6 +361,7 @@ async def revoke_auth_token(
|
||||
_MESSAGES_KEY = "chat:{session_id}:messages"
|
||||
_SESSIONS_SET = "chat:sessions"
|
||||
_SESSION_USER_KEY = "chat:{session_id}:user_id"
|
||||
_SESSION_TITLE_KEY = "chat:{session_id}:title"
|
||||
_USER_SESSIONS_KEY = "user:{user_id}:sessions"
|
||||
|
||||
|
||||
@@ -428,6 +429,31 @@ async def get_session_user_id(
|
||||
return await valkey.get(key)
|
||||
|
||||
|
||||
async def set_session_title(
|
||||
session_id: str,
|
||||
title: str,
|
||||
valkey: Redis | None = None,
|
||||
) -> None:
|
||||
"""Store a short human-readable title for a session."""
|
||||
if valkey is None:
|
||||
valkey = await get_valkey()
|
||||
|
||||
key = _SESSION_TITLE_KEY.format(session_id=session_id)
|
||||
await valkey.set(key, title)
|
||||
|
||||
|
||||
async def get_session_title(
|
||||
session_id: str,
|
||||
valkey: Redis | None = None,
|
||||
) -> str | None:
|
||||
"""Return the title for a session, or ``None`` if not set."""
|
||||
if valkey is None:
|
||||
valkey = await get_valkey()
|
||||
|
||||
key = _SESSION_TITLE_KEY.format(session_id=session_id)
|
||||
return await valkey.get(key)
|
||||
|
||||
|
||||
async def list_sessions(
|
||||
valkey: Redis | None = None,
|
||||
) -> list[str]:
|
||||
@@ -466,9 +492,11 @@ async def delete_session(
|
||||
|
||||
key = _MESSAGES_KEY.format(session_id=session_id)
|
||||
session_user_key = _SESSION_USER_KEY.format(session_id=session_id)
|
||||
session_title_key = _SESSION_TITLE_KEY.format(session_id=session_id)
|
||||
async with valkey.pipeline(transaction=True) as pipe:
|
||||
pipe.delete(key)
|
||||
pipe.delete(session_user_key)
|
||||
pipe.delete(session_title_key)
|
||||
pipe.srem(_SESSIONS_SET, session_id)
|
||||
if user_sessions_key is not None:
|
||||
pipe.srem(user_sessions_key, session_id)
|
||||
|
||||
@@ -16,6 +16,7 @@ from backend.core.database import (
|
||||
delete_user as db_delete_user,
|
||||
get_admin_stats,
|
||||
get_session_messages,
|
||||
get_session_title,
|
||||
get_session_user_id,
|
||||
get_user,
|
||||
get_user_by_username,
|
||||
@@ -95,6 +96,7 @@ class AdminUserUpdate(BaseModel):
|
||||
|
||||
class AdminSessionOut(BaseModel):
|
||||
session_id: str
|
||||
title: str | None = None
|
||||
message_count: int
|
||||
user_id: str | None = None
|
||||
|
||||
@@ -194,9 +196,11 @@ async def admin_list_sessions(
|
||||
for sid in ids:
|
||||
msgs = await get_session_messages(sid)
|
||||
uid = await get_session_user_id(sid)
|
||||
title = await get_session_title(sid)
|
||||
result.append(
|
||||
AdminSessionOut(
|
||||
session_id=sid,
|
||||
title=title,
|
||||
message_count=len(msgs),
|
||||
user_id=uid,
|
||||
)
|
||||
|
||||
+16
-1
@@ -14,10 +14,12 @@ from pydantic import BaseModel
|
||||
from backend.core.agent import AgentService
|
||||
from backend.core.database import (
|
||||
get_session_messages,
|
||||
get_session_title,
|
||||
get_session_user_id,
|
||||
list_sessions,
|
||||
resolve_auth_token,
|
||||
save_message,
|
||||
set_session_title,
|
||||
)
|
||||
from backend.core.dependencies import get_agent_service
|
||||
|
||||
@@ -43,6 +45,7 @@ class MessageOut(BaseModel):
|
||||
|
||||
class SessionOut(BaseModel):
|
||||
session_id: str
|
||||
title: str | None = None
|
||||
message_count: int
|
||||
user_id: str | None = None
|
||||
|
||||
@@ -89,6 +92,14 @@ async def chat_endpoint(
|
||||
user_id=user_id,
|
||||
)
|
||||
|
||||
# Generate a short title from the first user message if not yet set.
|
||||
existing_title = await get_session_title(session_id)
|
||||
if existing_title is None:
|
||||
title = body.message.strip()[:60]
|
||||
if len(body.message.strip()) > 60:
|
||||
title += "…"
|
||||
await set_session_title(session_id, title or "New chat")
|
||||
|
||||
# Ask the agent.
|
||||
output = await agent.ask(body.message, session_id=session_id)
|
||||
|
||||
@@ -133,9 +144,13 @@ async def sessions_list(
|
||||
for sid in ids:
|
||||
msgs = await get_session_messages(sid)
|
||||
uid = await get_session_user_id(sid)
|
||||
title = await get_session_title(sid)
|
||||
result.append(
|
||||
SessionOut(
|
||||
session_id=sid, message_count=len(msgs), user_id=uid
|
||||
session_id=sid,
|
||||
title=title,
|
||||
message_count=len(msgs),
|
||||
user_id=uid,
|
||||
)
|
||||
)
|
||||
return result
|
||||
|
||||
@@ -14,6 +14,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from backend.core.database import (
|
||||
create_user,
|
||||
get_session_title,
|
||||
get_user,
|
||||
get_user_by_username,
|
||||
list_user_sessions,
|
||||
@@ -55,6 +56,7 @@ class UserOut(BaseModel):
|
||||
|
||||
class UserSessionOut(BaseModel):
|
||||
session_id: str
|
||||
title: str | None = None
|
||||
|
||||
|
||||
# ── Helpers ────────────────────────────────────────────────────────────────
|
||||
@@ -145,4 +147,8 @@ async def user_sessions_endpoint(
|
||||
) -> list[UserSessionOut]:
|
||||
"""List all session IDs associated with a user."""
|
||||
session_ids = await list_user_sessions(str(user_id))
|
||||
return [UserSessionOut(session_id=sid) for sid in session_ids]
|
||||
result: list[UserSessionOut] = []
|
||||
for sid in session_ids:
|
||||
title = await get_session_title(sid)
|
||||
result.append(UserSessionOut(session_id=sid, title=title))
|
||||
return result
|
||||
|
||||
+11
-3
@@ -421,6 +421,7 @@ function SessionsTab({
|
||||
<table className="w-full text-left text-sm">
|
||||
<thead>
|
||||
<tr className="border-b border-gray-800 text-xs uppercase tracking-wider text-gray-500">
|
||||
<th className="pb-3 pr-4">Title</th>
|
||||
<th className="pb-3 pr-4">Session ID</th>
|
||||
<th className="pb-3 pr-4">Messages</th>
|
||||
<th className="pb-3 pr-4">User ID</th>
|
||||
@@ -430,7 +431,7 @@ function SessionsTab({
|
||||
<tbody>
|
||||
{sessions.length === 0 ? (
|
||||
<tr>
|
||||
<td colSpan={4} className="py-8 text-center text-gray-500">
|
||||
<td colSpan={5} className="py-8 text-center text-gray-500">
|
||||
No sessions yet.
|
||||
</td>
|
||||
</tr>
|
||||
@@ -440,9 +441,16 @@ function SessionsTab({
|
||||
key={s.session_id}
|
||||
className="border-b border-gray-800/50 transition hover:bg-gray-800/30"
|
||||
>
|
||||
<td className="max-w-48 py-3 pr-4">
|
||||
<p className="truncate text-sm text-gray-200">
|
||||
{s.title ?? (
|
||||
<span className="text-gray-500 italic">Untitled</span>
|
||||
)}
|
||||
</p>
|
||||
</td>
|
||||
<td className="py-3 pr-4">
|
||||
<code className="text-xs text-gray-300">
|
||||
{s.session_id.slice(0, 16)}…
|
||||
<code className="text-xs text-gray-500">
|
||||
{s.session_id.slice(0, 12)}…
|
||||
</code>
|
||||
</td>
|
||||
<td className="py-3 pr-4 text-gray-400">{s.message_count}</td>
|
||||
|
||||
+38
-7
@@ -310,6 +310,17 @@ export default function App() {
|
||||
|
||||
/* ── Session handlers ────────────────────────────────────────────── */
|
||||
|
||||
const handleNewSession = () => {
|
||||
localStorage.removeItem(STORAGE_SESSION_KEY);
|
||||
setSessionId("");
|
||||
setMessages([
|
||||
{
|
||||
role: "assistant",
|
||||
content: "Hello! I'm **Agent Alpha**. How can I help you today?",
|
||||
},
|
||||
]);
|
||||
};
|
||||
|
||||
const handleViewSessions = async () => {
|
||||
if (!authenticated) return;
|
||||
try {
|
||||
@@ -430,6 +441,15 @@ export default function App() {
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* New Chat button */}
|
||||
<button
|
||||
onClick={handleNewSession}
|
||||
className="rounded-lg border border-gray-700 px-3 py-1.5 text-sm text-gray-400 transition hover:border-indigo-500 hover:text-indigo-400"
|
||||
title="Start a new chat"
|
||||
>
|
||||
+ New Chat
|
||||
</button>
|
||||
|
||||
{/* Sessions button */}
|
||||
<button
|
||||
onClick={handleViewSessions}
|
||||
@@ -505,7 +525,7 @@ export default function App() {
|
||||
</div>
|
||||
<div className="flex-1 truncate">
|
||||
<p className="text-sm text-gray-200">
|
||||
{s.session_id.slice(0, 16)}…
|
||||
{s.title ?? s.session_id.slice(0, 16) + "…"}
|
||||
</p>
|
||||
</div>
|
||||
<span className="text-xs text-gray-500">
|
||||
@@ -515,12 +535,23 @@ export default function App() {
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<button
|
||||
onClick={() => setShowSessions(false)}
|
||||
className="mt-4 w-full rounded-lg border border-gray-700 px-4 py-2 text-sm text-gray-400 transition hover:bg-gray-800"
|
||||
>
|
||||
Close
|
||||
</button>
|
||||
<div className="mt-4 flex gap-2">
|
||||
<button
|
||||
onClick={() => {
|
||||
setShowSessions(false);
|
||||
handleNewSession();
|
||||
}}
|
||||
className="flex-1 rounded-lg border border-indigo-700 px-4 py-2 text-sm text-indigo-400 transition hover:bg-indigo-900/30"
|
||||
>
|
||||
+ New Chat
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setShowSessions(false)}
|
||||
className="flex-1 rounded-lg border border-gray-700 px-4 py-2 text-sm text-gray-400 transition hover:bg-gray-800"
|
||||
>
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -36,6 +36,7 @@ export interface MessageData {
|
||||
|
||||
export interface SessionData {
|
||||
session_id: string;
|
||||
title: string | null;
|
||||
message_count: number;
|
||||
user_id: string | null;
|
||||
}
|
||||
@@ -74,6 +75,7 @@ export interface UserCreateData {
|
||||
|
||||
export interface UserSessionData {
|
||||
session_id: string;
|
||||
title: string | null;
|
||||
}
|
||||
|
||||
/* ── Auth API ──────────────────────────────────────────────────── */
|
||||
@@ -241,6 +243,7 @@ export interface AdminUserData {
|
||||
|
||||
export interface AdminSessionData {
|
||||
session_id: string;
|
||||
title: string | null;
|
||||
message_count: number;
|
||||
user_id: string | null;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user