Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions extensions/Aditya2600/superdocsr/.Rbuildignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
^.*\.Rproj$
^\.Rproj\.user$
^\.github$
^LICENSE\.md$
^cran-comments\.md$
^\.DS_Store$
33 changes: 33 additions & 0 deletions extensions/Aditya2600/superdocsr/DESCRIPTION
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
Package: superdocsr
Title: Review-Gated AI Document Editing for Research Papers via SuperDocs
Version: 0.1.0
Authors@R:
person("Aditya", "Meshram", email = "adime2500@gmail.com", role = c("aut", "cre"))
Description: A researcher-facing client for the SuperDocs document editing
API. Renders an R Markdown paper with 'rmarkdown', uploads it, asks the
SuperDocs agent for a targeted edit, and returns the proposed changes for
inspection. Nothing is written into the document until the changes are
approved explicitly, so an AI edit can never land unreviewed. Long edits
are polled with exponential backoff under a wall-clock timeout, and a
per-client operation budget stops runaway loops before they spend a quota.
License: MIT + file LICENSE
Encoding: UTF-8
URL: https://github.com/superdocsapp/superdocs-builds
BugReports: https://github.com/superdocsapp/superdocs-builds/issues
Depends:
R (>= 4.1)
Imports:
curl,
httr2 (>= 1.0.0),
jsonlite (>= 1.8.0),
tools,
utils
Suggests:
knitr,
rmarkdown,
testthat (>= 3.0.0),
withr
VignetteBuilder: knitr
Config/testthat/edition: 3
Roxygen: list(markdown = TRUE)
Config/roxygen2/version: 8.1.0
2 changes: 2 additions & 0 deletions extensions/Aditya2600/superdocsr/LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
YEAR: 2026
COPYRIGHT HOLDER: Aditya Meshram
29 changes: 29 additions & 0 deletions extensions/Aditya2600/superdocsr/NAMESPACE
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# Generated by roxygen2: do not edit by hand

S3method("[",sd_changes)
S3method(print,sd_budget)
S3method(print,sd_changes)
S3method(print,sd_client)
S3method(print,sd_document)
S3method(print,sd_job)
S3method(print,sd_knit_result)
export(sd_approve)
export(sd_awaiting_kind)
export(sd_budget)
export(sd_cancel)
export(sd_changes)
export(sd_client)
export(sd_continue)
export(sd_deny)
export(sd_document)
export(sd_edit)
export(sd_export)
export(sd_job)
export(sd_knit)
export(sd_ops_remaining)
export(sd_ops_used)
export(sd_review_console)
export(sd_review_none)
export(sd_upload)
export(sd_verify_key)
export(sd_wait)
28 changes: 28 additions & 0 deletions extensions/Aditya2600/superdocsr/NEWS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# superdocsr 0.1.0

First release.

* `sd_client()` builds an authenticated SuperDocs client. The HTTP transport is
a plain function stored on the client, so tests and vignettes can swap in a
fake one and run with no network and no API key.
* `sd_upload()` loads a `.docx`/`.pdf`/`.html`/`.md`/`.txt`/`.rtf` file as the
active document of a session, streaming the file from disk rather than
holding it in memory.
* `sd_edit()` starts a review-gated AI edit. `approval_mode` defaults to
`"ask_every_time"`; auto-apply has to be asked for by name.
* `sd_wait()` polls `GET /v1/jobs/{job_id}` with exponential backoff and a
wall-clock timeout, and stops on every terminal or paused state instead of
spinning.
* `sd_changes()` returns the proposed changes as a data frame. Batches that
arrive as a JSON-encoded string in `content` are parsed a second time with
`jsonlite::fromJSON()`, which is what keeps every field from reading as `NA`.
* `sd_approve()` approves or denies changes by id, always sending the
top-level `approved` field the endpoint requires.
* `sd_continue()` answers the large-edit continue prompt, so a paused job is
resumed or stopped deliberately rather than waited out.
* `sd_export()` writes the approved document to disk as `docx`, `pdf`, `html`,
`markdown`, or `txt`, and surfaces the `X-Export-Warnings` header.
* `sd_knit()` renders an `.Rmd` with `rmarkdown::render()` and runs the same
upload, edit, review, export workflow over the result.
* `sd_budget()` caps billable operations per client and refuses documents above
an estimated page count, so a loop cannot quietly drain a quota.
108 changes: 108 additions & 0 deletions extensions/Aditya2600/superdocsr/R/approve.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
# The gate. Nothing in this package writes to a document except through here.

#' Approve or deny proposed changes
#'
#' Sends a decision for the change ids you name to
#' `POST /v1/chat/{session_id}/approve`. Approved changes are applied
#' atomically; denied ones are discarded, and any `feedback` goes back to the
#' AI, which may then propose a revision. After the call the job resumes, so
#' poll it again with [sd_wait()] -- a review can go several rounds.
#'
#' `change_ids` has no default on purpose. There is no "approve everything"
#' switch in this package, because an unreviewed AI edit to a paper you are
#' about to submit is exactly the failure this workflow exists to prevent. To
#' accept a whole batch, say so explicitly:
#' `sd_approve(job, sd_changes(job)$change_id)`.
#'
#' @param job An [sd_job()] paused on a change review. Check with
#' [sd_awaiting_kind()] first; a continue prompt is answered by
#' [sd_continue()] and rejects an approval with a 409.
#' @param change_ids Character vector of `change_id` values from [sd_changes()].
#' @param approved `TRUE` to apply them, `FALSE` to discard them.
#' @param feedback Optional note to the AI, most useful alongside
#' `approved = FALSE`: say what to do instead and the next proposal reflects
#' it.
#'
#' @return The refreshed [sd_job()], invisibly.
#' @seealso [sd_deny()] for the denying half.
#' @export
#' @examples
#' \dontrun{
#' changes <- sd_changes(job)
#' keep <- changes$change_id[changes$operation == "edit"]
#' job <- sd_approve(job, keep)
#' job <- sd_wait(job)
#' }
sd_approve <- function(job, change_ids, approved = TRUE, feedback = NULL) {
if (!inherits(job, "sd_job")) {
stop("`job` must come from sd_edit() or sd_job().", call. = FALSE)
}
if (missing(change_ids)) {
stop(
"Name the changes you are deciding on.\n",
" Cause: superdocsr has no approve-everything default; an unreviewed edit ",
"is the thing this workflow exists to prevent.\n",
" Fix: sd_approve(job, sd_changes(job)$change_id) to accept the whole batch, ",
"or pass the subset you agree with.",
call. = FALSE
)
}
change_ids <- unique(as.character(change_ids))
change_ids <- change_ids[!is.na(change_ids) & nzchar(change_ids)]
if (length(change_ids) == 0L) {
stop("`change_ids` is empty; there is nothing to decide.", call. = FALSE)
}
if (!is.logical(approved) || length(approved) != 1L || is.na(approved)) {
stop("`approved` must be TRUE or FALSE.", call. = FALSE)
}
if (is.na(job$job_id)) {
stop(
"This job has no job_id, so there is nothing to approve.\n",
" Cause: a synchronous sd_edit(async = FALSE) turn applies its changes and returns; ",
"there is no review stage to gate.\n",
" Fix: use async = TRUE with approval_mode = \"ask_every_time\" to review edits.",
call. = FALSE
)
}
if (identical(sd_awaiting_kind(job), "continue_prompt")) {
stop(
"This job is paused on a continue prompt, not a change review.\n",
" Fix: answer it with sd_continue(job, proceed = TRUE/FALSE). ",
"Approving a continue prompt is rejected with a 409.",
call. = FALSE
)
}

# `approved` is required at the top level even when every entry in `changes`
# carries its own. Omitting it is a documented 422.
body <- sd_drop_null(list(
job_id = job$job_id,
approved = approved,
feedback = feedback,
changes = lapply(change_ids, function(id) list(change_id = id, approved = approved))
))

req <- httr2::req_body_json(
sd_req(job$client, paste0("/v1/chat/", job$session_id, "/approve"), method = "POST"),
body,
auto_unbox = TRUE
)
sd_perform(job$client, req)
invisible(sd_job_refresh(job))
}

#' Deny proposed changes
#'
#' Shorthand for `sd_approve(approved = FALSE)`. Denying with feedback is the
#' useful shape: the AI reads it and may propose a revision, so keep polling.
#'
#' @inheritParams sd_approve
#' @return The refreshed [sd_job()], invisibly.
#' @export
#' @examples
#' \dontrun{
#' job <- sd_deny(job, "ch_3", feedback = "Keep the hedge; the effect is not significant.")
#' }
sd_deny <- function(job, change_ids, feedback = NULL) {
sd_approve(job, change_ids, approved = FALSE, feedback = feedback)
}
170 changes: 170 additions & 0 deletions extensions/Aditya2600/superdocsr/R/budget.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
# Spending controls. Everything that can cost money or spin in a loop passes
# through here first.

#' Cap what a client may spend
#'
#' A SuperDocs operation is billable; uploads, exports and job polls are not.
#' An edit loop that goes wrong is therefore the only thing that can quietly
#' drain a quota, and this is the object that stops it.
#'
#' Three controls, all enforced before a request leaves your machine:
#'
#' * `max_operations` -- the hard stop. Each [sd_edit()] spends one operation
#' from the client's allowance. When the allowance reaches zero the next edit
#' raises an error instead of sending the request.
#' * `max_pages` -- the size gate. A document above this estimated page count
#' is refused before any edit runs. Uploads are free, so this check costs
#' nothing and happens right after the upload, in [sd_upload()].
#' * `small_sample` -- the cheap preset for a first run: one operation, three
#' pages, and the fastest model tier.
#'
#' @section How pages are estimated:
#' The API reports `chunks_count` -- the number of addressable blocks
#' (paragraphs, headings, table rows and cells) it parsed out of the file. It
#' does not report a page count, and page count is not knowable from a `.docx`
#' without rendering it. `max_pages` therefore compares against
#' `ceiling(chunks_count / chunks_per_page)`, an estimate. The default of 12
#' chunks per page suits a double-spaced manuscript; a dense two-column paper
#' runs higher. Tune `chunks_per_page`, or set `max_pages = NULL` to switch the
#' gate off and rely on `max_operations` alone.
#'
#' @param max_operations Maximum billable operations this client may spend.
#' `Inf` removes the cap.
#' @param max_pages Maximum estimated pages a document may have before an edit
#' is allowed. `NULL` disables the gate.
#' @param chunks_per_page Chunks-per-page divisor used for the estimate above.
#' @param small_sample If `TRUE`, apply the cheap preset: at most one
#' operation, at most `small_sample_pages` pages, and `model_tier = "turbo"`
#' unless [sd_edit()] is told otherwise.
#' @param small_sample_pages Page cap used when `small_sample = TRUE`.
#'
#' @return An object of class `sd_budget`.
#' @export
#' @examples
#' sd_budget()
#' sd_budget(small_sample = TRUE)
#' sd_budget(max_operations = 3, max_pages = 40)
sd_budget <- function(max_operations = 25,
max_pages = NULL,
chunks_per_page = 12,
small_sample = FALSE,
small_sample_pages = 3) {
if (!is.numeric(max_operations) || length(max_operations) != 1L || max_operations < 0) {
stop("`max_operations` must be a single non-negative number.", call. = FALSE)
}
if (!is.null(max_pages) && (!is.numeric(max_pages) || length(max_pages) != 1L || max_pages < 1)) {
stop("`max_pages` must be NULL or a single number >= 1.", call. = FALSE)
}
if (!is.numeric(chunks_per_page) || length(chunks_per_page) != 1L || chunks_per_page < 1) {
stop("`chunks_per_page` must be a single number >= 1.", call. = FALSE)
}

if (isTRUE(small_sample)) {
max_operations <- min(max_operations, 1)
max_pages <- min(max_pages %||% small_sample_pages, small_sample_pages)
}

state <- new.env(parent = emptyenv())
state$used <- 0L

structure(
list(
max_operations = max_operations,
max_pages = max_pages,
chunks_per_page = chunks_per_page,
small_sample = isTRUE(small_sample),
state = state
),
class = "sd_budget"
)
}

#' @export
print.sd_budget <- function(x, ...) {
cat(" budget: ", x$state$used, " of ",
if (is.finite(x$max_operations)) x$max_operations else "unlimited",
" operations used",
if (x$small_sample) " (small sample)" else "",
"\n",
sep = ""
)
if (!is.null(x$max_pages)) {
cat(" max ", x$max_pages, " estimated pages per document\n", sep = "")
}
invisible(x)
}

#' Operations spent and left on a client
#'
#' @param client An [sd_client()].
#' @return A single number: operations spent so far, or operations still
#' available (possibly `Inf`).
#' @export
#' @examples
#' client <- sd_client(api_key = "sk_x", transport = function(req) req)
#' sd_ops_used(client)
#' sd_ops_remaining(client)
sd_ops_used <- function(client) {
sd_stopifnot_client(client)
client$budget$state$used
}

#' @rdname sd_ops_used
#' @export
sd_ops_remaining <- function(client) {
sd_stopifnot_client(client)
client$budget$max_operations - client$budget$state$used
}

# Spend n operations or refuse. Called before the request, so a refusal costs
# nothing at all.
sd_spend <- function(client, n = 1L, what = "edit") {
budget <- client$budget
if (budget$state$used + n > budget$max_operations) {
stop(
structure(
class = c("sd_budget_error", "error", "condition"),
list(
message = paste0(
"Operation budget exhausted: this client has spent ",
budget$state$used, " of ", budget$max_operations,
" operations and the ", what, " needs ", n, " more.\n",
" Fix: raise it with sd_client(budget = sd_budget(max_operations = N)), ",
"or start a new client. The cap exists so a loop cannot drain your quota."
),
call = NULL
)
)
)
}
budget$state$used <- budget$state$used + n
invisible(client)
}

# Refuse an oversized document. Uploads are free, so this runs after the upload
# and before anything billable.
sd_check_pages <- function(client, chunks_count, filename = "the document") {
budget <- client$budget
if (is.null(budget$max_pages) || is.null(chunks_count) || is.na(chunks_count)) {
return(invisible(NULL))
}
pages <- ceiling(chunks_count / budget$chunks_per_page)
if (pages > budget$max_pages) {
stop(
structure(
class = c("sd_budget_error", "error", "condition"),
list(
message = paste0(
filename, " parsed into ", chunks_count, " chunks, about ", pages,
" pages at ", budget$chunks_per_page, " chunks per page. ",
"The budget allows ", budget$max_pages, ".\n",
" Fix: raise max_pages in sd_budget(), tune chunks_per_page for this ",
"document's density, or edit a shorter excerpt first."
),
call = NULL
)
)
)
}
invisible(pages)
}
Loading