Odysseus v1.0
This commit is contained in:
213
src/session_actions.py
Normal file
213
src/session_actions.py
Normal file
@@ -0,0 +1,213 @@
|
||||
"""
|
||||
session_actions.py
|
||||
|
||||
Reusable session actions that can be called from both REST routes
|
||||
and the task scheduler / builtin actions system.
|
||||
"""
|
||||
|
||||
import json
|
||||
import logging
|
||||
import re
|
||||
from datetime import datetime
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Names that indicate a throwaway/test session
|
||||
_THROWAWAY_NAMES = {
|
||||
"test", "testing", "asdf", "asd", "hello", "hi", "hey",
|
||||
"yo", "sup", "hola", "hii", "hiii", "heyo",
|
||||
"foo", "bar", "baz", "tmp", "temp", "scratch", "untitled",
|
||||
"new chat", "delete", "remove", "junk", "trash", "xxx",
|
||||
"abc", "qwerty", "blah", "stuff", "whatever", "idk",
|
||||
"ok", "lol", "bruh", "hmm", "hm", "meh",
|
||||
}
|
||||
_THROWAWAY_MAX_MESSAGES = 4
|
||||
|
||||
|
||||
async def run_auto_sort(owner: str, skip_llm: bool = False) -> str:
|
||||
"""Run session cleanup + (optional) AI folder sort for the given owner.
|
||||
|
||||
Args:
|
||||
owner: user whose sessions to process
|
||||
skip_llm: when True, do only Phase 1 (delete empty/throwaway sessions);
|
||||
skip Phase 2 (AI folder assignment). Used by the built-in daily
|
||||
background sweep so it never burns LLM tokens.
|
||||
|
||||
Returns a human-readable summary of what was done.
|
||||
"""
|
||||
from core.database import SessionLocal, Session as DbSession, ChatMessage as DbMsg
|
||||
from src.llm_core import llm_call_async
|
||||
from src.task_endpoint import resolve_task_endpoint
|
||||
|
||||
db = SessionLocal()
|
||||
try:
|
||||
# ── Phase 1: Delete empty/throwaway sessions ──
|
||||
deleted_empty = 0
|
||||
deleted_throwaway = 0
|
||||
|
||||
rows = db.query(DbSession).filter(
|
||||
DbSession.archived == False,
|
||||
*([DbSession.owner == owner] if owner else []),
|
||||
).all()
|
||||
|
||||
for row in rows:
|
||||
if getattr(row, 'is_important', False):
|
||||
continue
|
||||
if (row.name or "").strip() == "Incognito":
|
||||
deleted_throwaway += 1
|
||||
db.delete(row)
|
||||
continue
|
||||
|
||||
msg_count = db.query(DbMsg.id).filter(
|
||||
DbMsg.session_id == row.id
|
||||
).limit(_THROWAWAY_MAX_MESSAGES + 1).count()
|
||||
should_delete = False
|
||||
|
||||
if msg_count == 0:
|
||||
should_delete = True
|
||||
deleted_empty += 1
|
||||
elif msg_count <= _THROWAWAY_MAX_MESSAGES:
|
||||
name = (row.name or "").strip().lower()
|
||||
first_msg = db.query(DbMsg.content).filter(
|
||||
DbMsg.session_id == row.id, DbMsg.role == "user"
|
||||
).order_by(DbMsg.timestamp).first()
|
||||
first_text = (first_msg[0] or "").strip().lower() if first_msg else ""
|
||||
assistant_count = db.query(DbMsg.id).filter(
|
||||
DbMsg.session_id == row.id, DbMsg.role == "assistant"
|
||||
).limit(1).count()
|
||||
|
||||
if name in _THROWAWAY_NAMES or name.startswith("chat:") or first_text in _THROWAWAY_NAMES:
|
||||
should_delete = True
|
||||
deleted_throwaway += 1
|
||||
elif msg_count == 1 and assistant_count == 0:
|
||||
should_delete = True
|
||||
deleted_throwaway += 1
|
||||
elif msg_count <= 4 and first_text and len(first_text.split()) <= 8 and len(first_text) <= 80:
|
||||
# Short trivial chats — e.g. "write hi to a friend" → "Hi!"
|
||||
should_delete = True
|
||||
deleted_throwaway += 1
|
||||
else:
|
||||
# Aggressive: total message text under 250 chars combined = trivial
|
||||
msg_rows = db.query(DbMsg.content).filter(
|
||||
DbMsg.session_id == row.id
|
||||
).all()
|
||||
total_chars = sum(len(m[0] or "") for m in msg_rows)
|
||||
if total_chars <= 250:
|
||||
should_delete = True
|
||||
deleted_throwaway += 1
|
||||
|
||||
if should_delete:
|
||||
db.delete(row)
|
||||
|
||||
if deleted_empty or deleted_throwaway:
|
||||
db.commit()
|
||||
logger.info(f"Auto-sort: deleted {deleted_empty} empty + {deleted_throwaway} throwaway sessions")
|
||||
|
||||
# ── Phase 2: AI folder assignment ──
|
||||
remaining = db.query(DbSession).filter(
|
||||
DbSession.archived == False,
|
||||
*([DbSession.owner == owner] if owner else []),
|
||||
).all()
|
||||
|
||||
session_list = []
|
||||
for row in remaining:
|
||||
if row.name == "Incognito":
|
||||
continue
|
||||
session_list.append({
|
||||
"id": row.id,
|
||||
"name": row.name or "(unnamed)",
|
||||
"current_folder": row.folder,
|
||||
})
|
||||
|
||||
if len(session_list) < 2:
|
||||
return f"Cleaned {deleted_empty + deleted_throwaway} sessions. Too few remaining to sort."
|
||||
|
||||
# Background built-in sweep skips folder-sort to stay pure infra.
|
||||
if skip_llm:
|
||||
return f"Cleaned {deleted_empty + deleted_throwaway} sessions (folder sort skipped)."
|
||||
|
||||
url, model, headers = resolve_task_endpoint()
|
||||
if not url:
|
||||
return f"Cleaned {deleted_empty + deleted_throwaway} sessions. No model endpoint available for sorting."
|
||||
|
||||
names_text = "\n".join(f' "{s["id"][:8]}": "{s["name"]}"' for s in session_list)
|
||||
prompt = (
|
||||
"You are a session organizer. Group these chat sessions into folders by topic.\n\n"
|
||||
"Rules:\n"
|
||||
"- Be aggressive about grouping — put EVERY session in a folder\n"
|
||||
"- Use short folder names (2-4 words max)\n"
|
||||
"- Use the 8-char ID prefixes exactly as given\n"
|
||||
"- Output ONLY raw JSON, no markdown fences, no explanation\n\n"
|
||||
"Required JSON format:\n"
|
||||
'{"folders": {"Folder Name": ["id_prefix1", "id_prefix2"], "Other Folder": ["id_prefix3"]}}\n\n'
|
||||
f"Sessions (id_prefix: name):\n{{\n{names_text}\n}}"
|
||||
)
|
||||
|
||||
try:
|
||||
# 16384 (was 4096): large folder JSON + reasoning-model thinking
|
||||
# overflowed 4096 and truncated the JSON, so it never parsed.
|
||||
raw = await llm_call_async(url, model, [{"role": "user", "content": prompt}],
|
||||
temperature=0.3, max_tokens=16384, headers=headers, timeout=120)
|
||||
except Exception as e:
|
||||
logger.warning(f"Auto-sort LLM call failed: {e}")
|
||||
return f"Cleaned {deleted_empty + deleted_throwaway} sessions. Folder sort skipped (model unreachable)."
|
||||
|
||||
# Parse JSON from response
|
||||
text = raw.strip()
|
||||
result = None
|
||||
try:
|
||||
result = json.loads(text)
|
||||
except json.JSONDecodeError:
|
||||
pass
|
||||
if result is None:
|
||||
fence_match = re.search(r'```(?:json)?\s*\n?([\s\S]*?)```', text)
|
||||
if fence_match:
|
||||
try:
|
||||
result = json.loads(fence_match.group(1).strip())
|
||||
except json.JSONDecodeError:
|
||||
pass
|
||||
if result is None:
|
||||
brace_start = text.find('{')
|
||||
brace_end = text.rfind('}')
|
||||
if brace_start >= 0 and brace_end > brace_start:
|
||||
try:
|
||||
result = json.loads(text[brace_start:brace_end + 1])
|
||||
except json.JSONDecodeError:
|
||||
pass
|
||||
if result is None:
|
||||
return f"Cleaned {deleted_empty + deleted_throwaway} sessions. AI returned unparseable response."
|
||||
|
||||
folders = result.get("folders", {})
|
||||
if not folders:
|
||||
return f"Cleaned {deleted_empty + deleted_throwaway} sessions. No folder groupings found."
|
||||
|
||||
# Apply assignments
|
||||
id_prefix_map = {s["id"][:8]: s["id"] for s in session_list}
|
||||
updated = 0
|
||||
for folder_name, ids in folders.items():
|
||||
for sid_or_prefix in ids:
|
||||
full_id = None
|
||||
if sid_or_prefix in id_prefix_map.values():
|
||||
full_id = sid_or_prefix
|
||||
else:
|
||||
prefix = sid_or_prefix.rstrip(".").rstrip(" ")
|
||||
if prefix in id_prefix_map:
|
||||
full_id = id_prefix_map[prefix]
|
||||
else:
|
||||
for p, fid in id_prefix_map.items():
|
||||
if fid.startswith(prefix) or prefix.startswith(p):
|
||||
full_id = fid
|
||||
break
|
||||
if full_id:
|
||||
db_sess = db.query(DbSession).filter(DbSession.id == full_id).first()
|
||||
if db_sess:
|
||||
db_sess.folder = folder_name
|
||||
db_sess.updated_at = datetime.utcnow()
|
||||
updated += 1
|
||||
db.commit()
|
||||
|
||||
folder_summary = ", ".join(f"{k} ({len(v)})" for k, v in folders.items())
|
||||
return f"Deleted {deleted_empty} empty + {deleted_throwaway} throwaway. Sorted {updated} sessions into: {folder_summary}"
|
||||
|
||||
finally:
|
||||
db.close()
|
||||
Reference in New Issue
Block a user