The AI copilot provides contextual chat for individual items (papers, websites, GitHub repos) and for project/library-wide notes. All copilot services use OpenAI tool calling to optionally produce note suggestions alongside conversational replies.
All copilot services expose two output tools to the model:
| Tool | Description |
|---|---|
suggest_note_edit |
Suggest a complete replacement of an existing note's content. Parameters: note_id, note_name, content (full HTML), description (1-2 sentences). |
suggest_note_create |
Suggest creating a new note file. Parameters: note_name, parent_id (nullable), content (HTML), description. |
When the model calls one of these tools, the service parses the arguments and returns a suggestion dict:
{
"id": "sug_<8hex>",
"status": "pending",
"type": "edit" | "create",
"noteId": "...", # edit only
"noteName": "...",
"content": "<p>...</p>",
"description": "..."
}Suggestions are persisted in the suggestions JSONB column of the chat_messages table alongside the assistant message.
File: backend/services/chat_service.py
API: POST /api/papers/:id/chat
Each completion call builds a message list:
- System prompt (
PAPER_CHATfromagents/prompts.py) - System message with paper context: title, abstract, and (if available) full PDF text extracted via
pdf_text_service.extract_and_cache() - System message with notes filesystem:
[folder]/[file]tree with note content up to 2000 chars per note - Last 20 conversation turns
Single-turn (no agentic loop): one chat.completions.create() call with tools. The response is either a text reply, one or more tool calls, or both. _process_tool_calls() extracts suggestions. If the model only calls tools with no text, a summary sentence is generated automatically ("Here are my suggestions: 2 edits and 1 new file.").
API: POST /api/websites/:id/chat
Same structure as paper chat. Context includes: title, URL, authors, description. No PDF text injection. Notes filesystem injected in the same format.
API: POST /api/github-repos/:id/chat
Context includes: owner/repo name, title, URL, language, topics, star count, abstract (from CITATION.cff) or description. Notes filesystem injected.
Service: backend/services/notes_copilot_service.py (library-scoped variant)
API: GET/POST/DELETE /api/libraries/:id/notes-copilot
The library copilot adds an @-mention context system. The request body includes:
{
"content": "user message",
"contextItems": [...],
"history": [{"role": "user", "content": "..."}, ...]
}contextItems are items the user selected via @ in the frontend. Each item has id, type, name, optional metadata, and optional notes (the item's notes tree with content). The backend builds a context block from these items and injects it as a system message.
This service uses the same single-turn pattern as the item copilots.
File: backend/services/project_notes_copilot_service.py
API: GET/POST/DELETE /api/projects/:id/notes-copilot
This is the most capable copilot variant. It runs an agentic loop with internal read tools in addition to the output tools.
| Tool | Description |
|---|---|
read_note |
Read the full HTML content of a note by ID. The model calls this before suggesting an edit to know current content. |
list_item_notes |
List the notes tree for any item type (paper, website, github_repo, library, project, experiment). |
The service loops up to _MAX_ITERATIONS = 6 times:
- Call
chat.completions.create()with all four tools. - If the response has no tool calls, stop.
- If it has tool calls, process each:
suggest_note_edit/suggest_note_create: add toall_suggestions, return confirmation text.read_note/list_item_notes: execute the read, return data as a tool result message.
- If only output tools were called (no internal tools), continue to next iteration.
- If only internal tools were called, continue looping (the model needs the data to proceed).
After the main loop, if the model produced text describing an intent ("I'll create a note...") but called no tools, a follow-up turn is sent:
"You described what you would do but didn't call any tool. Please call suggest_note_create or suggest_note_edit now."
This recovers cases where the model hallucinates the action without executing it.
If the loop produces neither text nor suggestions (e.g., the model only called output tools and the first turn was empty), a final call is made without tools to get a plain-text response.
The project copilot supports experiment type context items. Experiment metadata includes: status, config key-value pairs (up to 20), metrics key-value pairs (up to 20), and a summary of child experiments (up to 10 children with name, status, and top 3 metrics).
When a context item has include_pdf: true, the service calls pdf_text_service.extract_and_cache() and appends the PDF text (truncated at 15,000 chars) to the item's context block.
Chat history is stored in chat_messages with a project_id column (added in migration 020_project_notes_copilot.sql). The service degrades gracefully if the column is missing: history is not persisted between page reloads but the agentic response still works.
- Assistant message arrives with
suggestionsarray. CopilotPanelorNotesCopilotPanelrenders inline suggestion cards below the message bubble.- Each card shows: type badge (Edit / New Note), note name, description, and diff view (for edits) or preview (for creates).
- Diff view uses a simple line-based LCS diff on stripped HTML text.
- Accepting an edit calls
notesApi.update(noteId, { content })and invokesonNoteUpdated. - Accepting a create calls the appropriate
notesApi.createFor*()based ontargetTypeand invokesonNoteCreated. - Rejecting marks the suggestion card as rejected with a strikethrough.