mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-10-15 03:12:21 +02:00
feat(notes): attach selected Notes/Todos as Agent context (#6495)
* feat: add Notes and Todos as agent chat context - Send selected Note/Todo IDs from the Notes UI - Load selected notes with owner scoping in chat routes - Format note content and checklist items for the model - Inject selected notes as untrusted chat context - Add a selected-context indicator beside Shell Access - Keep selected context associated with the active chat session * Improved selected notes context handling and discouragin redundant note lookup and permission requests * Fix selected notes UI when switching chats * fix(notes): harden selected context lifecycle and validation --------- Co-authored-by: Alexandre Teixeira <111787685+alteixeira20@users.noreply.github.com>
This commit is contained in:
co-authored by
Alexandre Teixeira
parent
a8c147b238
commit
8feb50e894
@@ -1010,6 +1010,7 @@ async def build_chat_context(
|
||||
chat_processor,
|
||||
message: str,
|
||||
session_id: str,
|
||||
selected_notes_context: str | None = None,
|
||||
preset_id=None,
|
||||
att_ids: list = None,
|
||||
use_web=None,
|
||||
@@ -1182,6 +1183,23 @@ async def build_chat_context(
|
||||
if search_context and allow_tool_preprocessing and not casual_low_signal:
|
||||
preface.append(untrusted_context_message("prefetched search context", search_context))
|
||||
|
||||
# Inject selected notes as context
|
||||
if selected_notes_context:
|
||||
selected_notes_context = (
|
||||
"The following selected notes and todos are reference context explicitly chosen "
|
||||
"by the user. Use them directly when relevant to the user's request. Do not "
|
||||
"ask for permission to use this context, and do not call note-management "
|
||||
"tools just to retrieve the same notes; use those tools only for a requested "
|
||||
"change or when this context is insufficient.\n\n"
|
||||
+ selected_notes_context
|
||||
)
|
||||
preface.append(
|
||||
untrusted_context_message(
|
||||
"Selected notes and todos",
|
||||
selected_notes_context,
|
||||
)
|
||||
)
|
||||
|
||||
# YouTube transcripts
|
||||
for transcript in preprocessed.youtube_transcripts:
|
||||
preface.append(untrusted_context_message("youtube transcript", transcript))
|
||||
|
||||
+82
-1
@@ -58,7 +58,7 @@ from src.auth_helpers import (
|
||||
from routes.session_routes import _verify_session_owner
|
||||
from routes.document_helpers import _owner_session_filter
|
||||
from core.database import SessionLocal, get_session_mode, set_session_mode
|
||||
from core.database import Session as DBSession, ChatMessage as DBChatMessage
|
||||
from core.database import Session as DBSession, ChatMessage as DBChatMessage, Note
|
||||
from core.database import Document as DBDocument, ModelEndpoint
|
||||
from core.log_safety import redact_url
|
||||
from routes.research_routes import _resolve_research_endpoint
|
||||
@@ -2153,6 +2153,68 @@ def _set_user_time_from_request(request: Request) -> None:
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def _parse_selected_note_ids(raw) -> list[str]:
|
||||
"""Validate user-selected Note IDs before passing them to SQLAlchemy."""
|
||||
if raw is None or raw == "":
|
||||
return []
|
||||
if not isinstance(raw, str) or len(raw) > 8192:
|
||||
raise HTTPException(400, "Invalid selected_note_ids payload")
|
||||
try:
|
||||
parsed = json.loads(raw)
|
||||
except (TypeError, ValueError) as exc:
|
||||
raise HTTPException(400, "Invalid selected_note_ids JSON") from exc
|
||||
if (
|
||||
not isinstance(parsed, list)
|
||||
or len(parsed) > 50
|
||||
or any(
|
||||
not isinstance(note_id, str)
|
||||
or not note_id.strip()
|
||||
or len(note_id) > 128
|
||||
for note_id in parsed
|
||||
)
|
||||
):
|
||||
raise HTTPException(400, "selected_note_ids must be an array of up to 50 non-empty string IDs")
|
||||
return list(dict.fromkeys(parsed))
|
||||
|
||||
|
||||
def _format_selected_notes_context(notes) -> str:
|
||||
"""Format selected Notes/Todos as reference context for the model."""
|
||||
sections = []
|
||||
|
||||
for index, note in enumerate(notes, start=1):
|
||||
lines = [
|
||||
f"--- Selected Note {index} ---",
|
||||
f"Title: {note.title or '(untitled)'}",
|
||||
f"Type: {note.note_type or 'note'}",
|
||||
]
|
||||
|
||||
if note.content:
|
||||
lines.extend([
|
||||
"Content:",
|
||||
str(note.content),
|
||||
])
|
||||
|
||||
if note.items:
|
||||
try:
|
||||
items = json.loads(note.items)
|
||||
except (TypeError, json.JSONDecodeError):
|
||||
items = []
|
||||
|
||||
if isinstance(items, list):
|
||||
lines.append("Items:")
|
||||
for item in items:
|
||||
if not isinstance(item, dict):
|
||||
continue
|
||||
|
||||
status = "done" if item.get("done") else "pending"
|
||||
text = str(item.get("text") or "").strip()
|
||||
|
||||
if text:
|
||||
lines.append(f"- [{status}] {text}")
|
||||
|
||||
sections.append("\n".join(lines))
|
||||
|
||||
return "\n\n".join(sections)
|
||||
|
||||
def _resolve_prompt_thinking_mode(explicit_mode, preset_id, preset_manager):
|
||||
"""Use an explicit request override, then fall back to the active preset."""
|
||||
@@ -2423,6 +2485,8 @@ def setup_chat_routes(
|
||||
form_data = await request.form()
|
||||
message = form_data.get("message")
|
||||
session = form_data.get("session")
|
||||
selected_note_ids = _parse_selected_note_ids(form_data.get("selected_note_ids"))
|
||||
|
||||
attachments = form_data.get("attachments")
|
||||
use_web = form_data.get("use_web")
|
||||
use_research = form_data.get("use_research")
|
||||
@@ -2730,6 +2794,22 @@ def setup_chat_routes(
|
||||
current_rejected=workspace_rejected,
|
||||
)
|
||||
owner = effective_user(request)
|
||||
selected_notes = []
|
||||
if selected_note_ids:
|
||||
notes_db = SessionLocal()
|
||||
try:
|
||||
selected_notes = (
|
||||
notes_db.query(Note)
|
||||
.filter(
|
||||
Note.id.in_(selected_note_ids),
|
||||
Note.owner == owner,
|
||||
)
|
||||
.all()
|
||||
)
|
||||
finally:
|
||||
notes_db.close()
|
||||
selected_notes_context = _format_selected_notes_context(selected_notes)
|
||||
|
||||
if tool_approval_id:
|
||||
_reject_delegated_tool_approval(request)
|
||||
from src.agent_runtime.authority import require_user_approval_request
|
||||
@@ -3001,6 +3081,7 @@ def setup_chat_routes(
|
||||
sess, request, chat_handler, chat_processor,
|
||||
message=message,
|
||||
session_id=session,
|
||||
selected_notes_context=selected_notes_context,
|
||||
preset_id=preset_id,
|
||||
att_ids=att_ids,
|
||||
use_web=use_web,
|
||||
|
||||
Reference in New Issue
Block a user