8000 feat(chat): export a conversation as Markdown, PDF or JSON by adeelahmad · Pull Request #474 · adeelahmad/lense · GitHub
[go: up one dir, main page]

Skip to content

feat(chat): export a conversation as Markdown, PDF or JSON - #474

Merged
adeelahmad merged 18 commits into
mainfrom
claude/project-thread-r960xc-c3-export
Oct 17, 2026
Merged

adeelahmad merged 18 commits into
mainfrom
claude/project-thread-r960xc-c3-export

Conversation

@adeelahmad
Copy link
Copy Markdown
Owner

Description

Before: a conversation could only be read in the web app. There was no way to keep it as a file or hand it to another tool, and agents couldn't read it.

After: GET /api/v1/chats/<id>/export.md, .pdf or .json downloads the version that's showing. ?message=<mid> downloads one message (an answer comes with its question), and on the JSON export ?all=true downloads every version.

  • Markdown has a header with the conversation's link, when it started, what it draws on and the Lens version. Then come each question and answer. Citations are numbered footnotes with a short quote and a link back to that moment in Lens (/resources/<id>?t=<s>), or to the web page. Tools the answer used appear as a "Looked up" list.
  • PDF is that Markdown (rendered by convert.markdown_page) printed by Chromium if it's installed. Otherwise anytopdf prints it, whether installed, on a conversion node, or fetched on first use unless Settings › Documents says never. Otherwise LibreOffice does. If none of them can run, or printing fails, the endpoint returns 409 with the reason. Nothing heavy was added.
  • JSON uses OpenAI's chat message format, laid out as a tree (mapping and current_node) with Dublin Core terms. An answer's tool calls go in tool_messages: an assistant tool_calls message, then one tool message per step carrying its summary. Citations, the answer's record, versions, files and the page it was asked from go under lens. ?format=messages gives a single {"messages": [...]} line instead. The schema is at docs/schemas/chat-export.schema.json, and the tests validate against it.
  • MCP gets three read-only tools: list_chats, read_chat and export_chat. A new Tool.personal flag keeps them off a namespace's own server.

Citations follow the reader's current access, the same way GET /chats/<id> does. No model is called.

Motivation and Context

Module C3 of the chat-gaps plan, covering copy and export. This PR is the backend and MCP part. The web app's copy buttons, code-block copy, Export menus and print fallback come in a stacked PR. Built on #469 (C1's answer record), with main merged in.

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change; ! in the title)

Checklist:

  • The title is a Conventional Commit.
  • My code follows the code style of this project (ruff, ruff format, mypy all clean).
  • I added or updated tests (tests/api/test_chat_export.py; test_mcp_namespace.py checks the personal tools stay off a namespace server).
  • My change requires documentation updates, and I have updated docs/ accordingly (docs/assistant.md › Copy and export, docs/mcp.md).
  • Users need more than the title to understand it, so I wrote it under ## Unreleased in CHANGELOG.md.

Done questions (AGENTS.md)

  • Goal: the backend part of copy and export is done. The web app comes in the stacked PR. This one merges on green CI.
  • Agents: the API endpoint, plus the MCP tools list_chats, read_chat and export_chat.
  • Zero setup: works right after install. A PDF uses whatever printer the server already has or can fetch.
  • Fewest clicks: n/a for this PR, which adds no UI (the UI is in the stacked PR).
  • Configurable: n/a, no new settings. The PDF follows the existing documents.converter setting.
  • Light: no new dependency. Chromium and LibreOffice are used only if they're already there, and anytopdf is fetched on first use.
  • Private and secure: an export is the person's own conversation and goes to them alone. The threat model gains an output row saying exports carry tool summaries and citations, never raw tool results. The MCP tools aren't on a namespace's server.
  • Cost: no model calls.
  • User decides: everything here reads; no defaults change.
  • Upgrades: n/a, no change to stored data.
  • Honest: answers carry their citations with links. Docs and CHANGELOG label the web app buttons as in progress and the refine-later items as planned.
  • Tests, docs and generated files updated: the OpenAPI schema and client were regenerated with make openapi's two commands.
  • Open standards: Dublin Core terms, OpenAI's chat message format and a JSON Schema (2020-12). No new dependency.
  • Result reported with evidence. Refine-later items are noted in docs/assistant.md: importing a conversation from JSON, exporting to a note, and a styled PDF without Chromium.

Local checks: targeted pytest -n 2 (65 passed), ruff, ruff format, mypy (151 files), tsc, mkdocs build --strict, ci/validate_threat_model.py, and the OpenAPI diff check.


Generated by Claude Code

Importing app.domain.ai_tools on its own failed: it imported automations,
which loads the actions, and the agent's action subclasses Toolbox before
ai_tools had defined it. Running one test file (tests/api/test_chats.py)
hit it. ai_tools now imports automations last.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HXPWXcSCQb9hWPr7xkZu7B
Every answer keeps `meta`: the model and the host name of its server,
the tools offered and asked for, the scope it answered in (after
auto-scope), its limits and what it used, how it ended (answered,
direct, limit, fallback, stopped, error), tool results that read like
instructions to an AI, how long it took, Lens's version, and tokens and
cost from the ledger rows that name it. Questions keep how they were
sent (web, api, voice, room, automation) and their options.

The answer's id is taken once the question is saved and the answer
runs in activity.scope(chat:<cid>, chat_message:<mid>), in the web
chat, chat rooms and the automations' agent; the streamed answer holds
the scope across its steps (activity.scoped). App completions keep no
messages, so their record comes back in lens.record. The stream sends
`meta` before `done`; GET /chats/{cid}/messages/{mid} returns one
message with its record and ledger rows. Each tool step keeps its time
and whether it failed. The web app shows it all from an i in the
answer's header (a sheet on a phone); old answers say they weren't
recorded.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HXPWXcSCQb9hWPr7xkZu7B
The last check asked for 2026-10-17's note after the call without a date had made today's note, so on that day it
found the note the deleted root had left behind. It now asks for a day no earlier call makes.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HXPWXcSCQb9hWPr7xkZu7B
The answer's record uses C2's reserved answer_id for its ledger scope and is saved under the question (parent);
a message of any version can be read with its record and ledger rows.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HXPWXcSCQb9hWPr7xkZu7B
GET /api/v1/chats/{cid}/export.{md,json,pdf} gives the version showing,
one message (?message=), or with JSON every version (?all=true) and the
OpenAI {"messages": [...]} shape (?format=messages). Markdown carries
citations as footnotes linking back to Lens and the tools used as a
"Looked up" list; JSON is a tree of OpenAI chat messages with Dublin
Core terms, published as docs/schemas/chat-export.schema.json; the PDF
is that Markdown printed by Chromium, else anytopdf, else LibreOffice,
and 409 when none can run. MCP gains list_chats, read_chat and
export_chat, kept off a namespace's own server.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HXPWXcSCQb9hWPr7xkZu7B
…record' into claude/project-thread-r960xc-c3-export
The threat model lists a conversation's export as an output: the
person's own, with tool summaries and citations, never raw tool results.
The OpenAPI schema and client are regenerated with make openapi's two
commands.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HXPWXcSCQb9hWPr7xkZu7B
@adeelahmad adeelahmad self-assigned this Oct 17, 2026
The model sometimes cites several passages in one bracket, as the web
app already shows; each cited number now becomes its own footnote, and
a number with no passage stays as written.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HXPWXcSCQb9hWPr7xkZu7B
C1 (#469), web search (#472) and the sandbox (#473) are in main. The
routes keep C5's web pages in citations alongside the export, and the
export test now checks a cited web page stays through access changes.
The OpenAPI schema and client are regenerated with make openapi's two
commands, and the threat model's prose_version is recomputed.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HXPWXcSCQb9hWPr7xkZu7B
Keeps both changelog entries and recomputes the threat model's
prose_version; the OpenAPI schema and client regenerate unchanged.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HXPWXcSCQb9hWPr7xkZu7B
On a loaded runner the merge picker's options were checked while the
list before the typing settled was still showing; the lookup for the
typed words then emptied it, and the next check found no option. The
test now waits for that lookup and checks the options in one step.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HXPWXcSCQb9hWPr7xkZu7B
…-r960xc-c3-export

# Conflicts:
#	CHANGELOG.md
#	docs/mcp.md
#	nextjs-frontend/__tests__/entities-curate.test.tsx
#	threat-model.yaml
…-r960xc-c3-export

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HXPWXcSCQb9hWPr7xkZu7B
…-r960xc-c3-export

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HXPWXcSCQb9hWPr7xkZu7B
@adeelahmad
adeelahmad merged commit 90f1060 into main Oct 17, 2026
12 checks passed
@adeelahmad
adeelahmad deleted the claude/project-thread-r960xc-c3-export branch October 17, 2026 08:13
adeelahmad pushed a commit that referenced this pull request Oct 17, 2026
main now carries the export API as one squashed commit (#474), so this keeps only the web app's copy and
export on top of it.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HXPWXcSCQb9hWPr7xkZu7B
adeelahmad pushed a commit that referenced this pull request Oct 17, 2026
Brings in conversation export (#474, #484). Exports keep a docs or
server citation's source (lens_help) instead of calling it a web page;
the export schema lists both.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YHrWfYDCaCbKKDX4w9at9C
adeelahmad pushed a commit that referenced this pull request Oct 18, 2026
…ay and clipper sign-in as built

Since the last README refresh, #474, #484, #510, #511, #497, #495, #406, #516, #517
and #491 merged. The README's built list now says a conversation exports as Markdown,
PDF or JSON and its answers, questions and code copy; a Tools chip picks an answer's
tools, limit and namespaces; sandbox runs open under the answer with downloads, and
Settings starts the gateway; deep research plans, runs and returns a cited report;
ready-made MCP servers (the browser, GitHub) and the optional containers start from
the web app; and the web clipper signs in or takes an API token. Each has its checks
in docs/requirements.md. In progress drops the merged PRs and lists the open feature
PRs; the Planned proof item keeps only the Raspberry Pi timing (#481 and #482 are
closed); the docs index links requirements and checks, and published feeds.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HDx7PPUauyjMn1LZs8JAsS
adeelahmad pushed a commit that referenced this pull request Oct 18, 2026
…ay and clipper sign-in as built

Since th
8000
e last README refresh, #474, #484, #510, #511, #497, #495, #406, #516, #517,
#491, #400 and #506 merged. The README's built list now says the assistant answers
questions about Lens itself; a conversation exports as Markdown, PDF or JSON and its
answers, questions and code copy; a Tools chip picks an answer's tools, limit and
namespaces; sandbox runs open under the answer with downloads, and Settings starts the
gateway; deep research plans, runs and returns a cited report; ready-made MCP servers,
the MCP servers page and the optional containers start from the web app; and the web
clipper signs in or takes an API token. Each has its checks in docs/requirements.md.
In progress lists only open PRs; the Nebula mesh (#546, merged, partly built) moves to
Planned with what's next; the Planned proof item keeps only the Raspberry Pi timing;
the docs index links requirements and checks, and published feeds.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HDx7PPUauyjMn1LZs8JAsS
adeelahmad added a commit that referenced this pull request Oct 18, 2026
…ay and clipper sign-in as built (#536)

Since the last README refresh, #474, #484, #510, #511, #497, #495, #406, #516, #517,
#491, #400 and #506 merged. The README's built list now says the assistant answers
questions about Lens itself; a conversation exports as Markdown, PDF or JSON and its
answers, questions and code copy; a Tools chip picks an answer's tools, limit and
namespaces; sandbox runs open under the answer with downloads, and Settings starts the
gateway; deep research plans, runs and returns a cited report; ready-made MCP servers,
the MCP servers page and the optional containers start from the web app; and the web
clipper signs in or takes an API token. Each has its checks in docs/requirements.md.
In progress lists only open PRs; the Nebula mesh (#546, merged, partly built) moves to
Planned with what's next; the Planned proof item keeps only the Raspberry Pi timing;
the docs index links requirements and checks, and published feeds.


Claude-Session: https://claude.ai/code/session_01HDx7PPUauyjMn1LZs8JAsS

Co-authored-by: Adeel Ahmad <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

0