Skip to content
Open
Show file tree
Hide file tree
Changes from 6 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
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import app from "../../loopquest.app.mjs";

export default {
key: "loopquest-create-review-task",
name: "Create Review Task",
description: "Send AI/automation output to a human. Gate a downstream action until it's approved, or monitor quality in the background. [See the docs](https://loopquest.tomphillips.uk/docs).",
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
version: "0.0.1",
type: "action",
props: {
loopquest: app,
content: { propDefinition: [app, "content"] },
title: { propDefinition: [app, "title"] },
module: { propDefinition: [app, "module"] },
mode: { propDefinition: [app, "mode"] },
claim: { propDefinition: [app, "claim"] },
sourceText: { propDefinition: [app, "sourceText"] },
timeoutSeconds: { propDefinition: [app, "timeoutSeconds"] },
onTimeout: { propDefinition: [app, "onTimeout"] },
source: { propDefinition: [app, "source"] },
externalId: { propDefinition: [app, "externalId"] },
callbackUrl: { propDefinition: [app, "callbackUrl"] },
reviewsRequired: { propDefinition: [app, "reviewsRequired"] },
},
async run({ $ }) {
const res = await this.loopquest.createTask({
$,
props: {
content: this.content,
title: this.title,
module: this.module,
mode: this.mode,
claim: this.claim,
sourceText: this.sourceText,
timeoutSeconds: this.timeoutSeconds,
onTimeout: this.onTimeout,
source: this.source,
externalId: this.externalId,
callbackUrl: this.callbackUrl,
reviewsRequired: this.reviewsRequired,
},
});
$.export("$summary", `Submitted review task ${res.id}`);
return res;
},
};
Comment thread
coderabbitai[bot] marked this conversation as resolved.
18 changes: 18 additions & 0 deletions components/loopquest/actions/get-task-status/get-task-status.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import app from "../../loopquest.app.mjs";

export default {
key: "loopquest-get-task-status",
name: "Get Task Status",
description: "Check a LoopQuest task's status / verdict. [See the docs](https://loopquest.tomphillips.uk/docs).",
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
version: "0.0.1",
type: "action",
props: {
loopquest: app,
taskId: { propDefinition: [app, "taskId"] },
},
async run({ $ }) {
const res = await this.loopquest.getTask({ $, taskId: this.taskId });
$.export("$summary", `Task ${this.taskId} is ${res.status}`);
return res;
},
};
Comment thread
coderabbitai[bot] marked this conversation as resolved.
20 changes: 20 additions & 0 deletions components/loopquest/common/body.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/** Build the POST /api/v1/tasks body from action props. Pure — unit tested. */
export function buildTaskBody(p = {}) {
const payload = { content: p.content, body: p.content };
if (p.claim) payload.claim = p.claim;
if (p.sourceText) payload.source = p.sourceText;

const body = {
module: p.module || "swiper",
mode: p.mode || "monitor",
payload,
card: { title: p.title || "Review", body: p.content },
};
if (p.source) body.source = p.source;
if (p.externalId) body.external_id = p.externalId;
if (p.callbackUrl) body.callback_url = p.callbackUrl;
if (p.timeoutSeconds) body.timeout_seconds = p.timeoutSeconds;
if (p.onTimeout) body.on_timeout = p.onTimeout;
if (p.reviewsRequired) body.reviews_required = p.reviewsRequired;
Comment on lines +13 to +18

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Manual truthiness guards are unnecessary — rely on automatic undefined stripping.

Since these values are sent via @pipedream/platform's axios as a JSON data payload, undefined fields are dropped automatically during serialization. The if (p.x) body.x = ... guards can be replaced with direct unconditional assignment, per the guideline that optional props may be passed directly into request bodies without truthiness checks.

♻️ Proposed simplification
-  if (p.source) body.source = p.source;
-  if (p.externalId) body.external_id = p.externalId;
-  if (p.callbackUrl) body.callback_url = p.callbackUrl;
-  if (p.timeoutSeconds) body.timeout_seconds = p.timeoutSeconds;
-  if (p.onTimeout) body.on_timeout = p.onTimeout;
-  if (p.reviewsRequired) body.reviews_required = p.reviewsRequired;
+  body.source = p.source;
+  body.external_id = p.externalId;
+  body.callback_url = p.callbackUrl;
+  body.timeout_seconds = p.timeoutSeconds;
+  body.on_timeout = p.onTimeout;
+  body.reviews_required = p.reviewsRequired;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (p.source) body.source = p.source;
if (p.externalId) body.external_id = p.externalId;
if (p.callbackUrl) body.callback_url = p.callbackUrl;
if (p.timeoutSeconds) body.timeout_seconds = p.timeoutSeconds;
if (p.onTimeout) body.on_timeout = p.onTimeout;
if (p.reviewsRequired) body.reviews_required = p.reviewsRequired;
body.source = p.source;
body.external_id = p.externalId;
body.callback_url = p.callbackUrl;
body.timeout_seconds = p.timeoutSeconds;
body.on_timeout = p.onTimeout;
body.reviews_required = p.reviewsRequired;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@components/loopquest/common/body.mjs` around lines 13 - 18, In body.mjs, the
manual truthiness checks around optional request fields are unnecessary because
axios drops undefined values automatically. Update the body-building logic in
the body assembly block to assign p.source, p.externalId, p.callbackUrl,
p.timeoutSeconds, p.onTimeout, and p.reviewsRequired directly to body.source,
body.external_id, body.callback_url, body.timeout_seconds, body.on_timeout, and
body.reviews_required without wrapping them in if statements.

Source: Coding guidelines

return body;
}
89 changes: 89 additions & 0 deletions components/loopquest/loopquest.app.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import { axios } from "@pipedream/platform";
import { buildTaskBody } from "./common/body.mjs";

export default {
type: "app",
app: "loopquest",
propDefinitions: {
content: { type: "string", label: "Content", description: "The AI/automation output a human should review." },
title: { type: "string", label: "Title", optional: true },
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
module: {
type: "string",
label: "Game",
optional: true,
default: "swiper",
description: "How the reviewer sees the item.",
options: [
{ label: "Swiper — approve or reject", value: "swiper" },
{ label: "Versus — pick the better of two", value: "versus" },
{ label: "Sorter — bucket into categories", value: "sorter" },
{ label: "Detective — spot the problem", value: "detective" },
{ label: "Fixer — correct the output", value: "fixer" },
{ label: "Redact — mask sensitive text", value: "redact" },
{ label: "Grounding — verify a claim against a source", value: "grounding" },
],
},
mode: {
type: "string",
label: "Mode",
optional: true,
default: "monitor",
description: "`gate` blocks a downstream step until a human approves (pair with the New Verdict trigger). `monitor` reviews in the background without pausing.",
options: ["monitor", "gate"],
},
claim: { type: "string", label: "Claim", optional: true, description: "Grounding only: the statement to verify." },
sourceText: { type: "string", label: "Source Text", optional: true, description: "Grounding only: the reference text the claim is checked against." },
timeoutSeconds: { type: "integer", label: "Timeout (seconds)", optional: true, description: "Gate only: apply the fallback if no one reviews in time (30–2592000)." },
onTimeout: {
type: "string",
label: "On Timeout",
optional: true,
description: "Gate only: what to do if the timeout is hit. Defaults to escalate (fail-closed).",
options: ["escalate", "reject", "approve"],
},
source: { type: "string", label: "Source", optional: true, default: "pipedream", description: "A label for where this came from." },
externalId: { type: "string", label: "External ID", optional: true, description: "Your own id for the item — echoed back in the verdict so you can correlate it." },
callbackUrl: { type: "string", label: "Callback URL", optional: true, description: "Optional. A single webhook for this task's verdict. Leave blank if you use the New Verdict trigger." },
reviewsRequired: { type: "integer", label: "Reviewers Required", optional: true },
taskId: { type: "string", label: "Task ID" },
},
methods: {
_baseUrl() {
return (this.$auth.base_url || "https://loopquest.tomphillips.uk").replace(/\/+$/, "");
},
_headers() {
return { authorization: `Bearer ${this.$auth.api_key}`, "content-type": "application/json" };
},
async createTask({ $, props }) {
return axios($, {
method: "POST",
url: `${this._baseUrl()}/api/v1/tasks`,
headers: this._headers(),
data: buildTaskBody(props),
});
},
async getTask({ $, taskId }) {
return axios($, {
method: "GET",
url: `${this._baseUrl()}/api/v1/tasks/${taskId}`,
headers: this._headers(),
});
},
// Verdict subscriptions — power the New Verdict source (REST-hook style).
async subscribeVerdicts($, url) {
return axios($, {
method: "POST",
url: `${this._baseUrl()}/api/v1/hooks`,
headers: this._headers(),
data: { url },
});
},
async unsubscribeVerdicts($, id) {
return axios($, {
method: "DELETE",
url: `${this._baseUrl()}/api/v1/hooks/${id}`,
headers: this._headers(),
});
},
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
},
};
19 changes: 19 additions & 0 deletions components/loopquest/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"name": "@pipedream/loopquest",
"version": "0.0.1",
"description": "Pipedream components for LoopQuest — human-in-the-loop review.",
"main": "loopquest.app.mjs",
"type": "module",
"keywords": [
"pipedream",
"loopquest"
],
"homepage": "https://pipedream.com/apps/loopquest",
"author": "Pipedream <support@pipedream.com> (https://pipedream.com/)",
"publishConfig": {
"access": "public"
},
"dependencies": {
"@pipedream/platform": "^3.1.1"
}
}
46 changes: 46 additions & 0 deletions components/loopquest/sources/new-verdict/new-verdict.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import app from "../../loopquest.app.mjs";

export default {
key: "loopquest-new-verdict",
name: "New Verdict",
description: "Emit an event the moment a human reviewer resolves a task — approve, flag, escalate or timeout. Use it to resume a gated action or act on a monitored review. [See the docs](https://loopquest.tomphillips.uk/docs).",
version: "0.0.1",
type: "source",
dedupe: "unique",
props: {
loopquest: app,
http: { type: "$.interface.http", customResponse: true },
db: "$.service.db",
},
hooks: {
// Register this source's HTTP endpoint as a verdict subscription. LoopQuest
// then POSTs every resolved verdict here (idempotent by URL server-side).
async activate() {
const { id } = await this.loopquest.subscribeVerdicts(this, this.http.endpoint);
this.db.set("hookId", id);
},
async deactivate() {
const hookId = this.db.get("hookId");
if (hookId) await this.loopquest.unsubscribeVerdicts(this, hookId);
},
Comment on lines +18 to +25

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Encapsulate db access in private helper methods.

this.db.set/this.db.get are called directly inside activate()/deactivate(). Per path instructions, state stored in $.service.db should go through private helper methods rather than being called directly from event handlers.

♻️ Proposed refactor
+  methods: {
+    _getHookId() {
+      return this.db.get("hookId");
+    },
+    _setHookId(id) {
+      this.db.set("hookId", id);
+    },
+  },
   hooks: {
     async activate() {
       const { id } = await this.loopquest.subscribeVerdicts(this, this.http.endpoint);
-      this.db.set("hookId", id);
+      this._setHookId(id);
     },
     async deactivate() {
-      const hookId = this.db.get("hookId");
+      const hookId = this._getHookId();
       if (hookId) await this.loopquest.unsubscribeVerdicts(this, hookId);
     },
   },

As per path instructions, "State stored in $.service.db should be accessed through private methods (_getX(), _setX()), not called directly from run() or event handlers."

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
async activate() {
const { id } = await this.loopquest.subscribeVerdicts(this, this.http.endpoint);
this.db.set("hookId", id);
},
async deactivate() {
const hookId = this.db.get("hookId");
if (hookId) await this.loopquest.unsubscribeVerdicts(this, hookId);
},
methods: {
_getHookId() {
return this.db.get("hookId");
},
_setHookId(id) {
this.db.set("hookId", id);
},
},
async activate() {
const { id } = await this.loopquest.subscribeVerdicts(this, this.http.endpoint);
this._setHookId(id);
},
async deactivate() {
const hookId = this._getHookId();
if (hookId) await this.loopquest.unsubscribeVerdicts(this, hookId);
},
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@components/loopquest/sources/new-verdict/new-verdict.mjs` around lines 18 -
25, Direct `this.db.set`/`this.db.get` usage in `activate()` and `deactivate()`
violates the state access pattern for `$.service.db`. Add private helper methods
on the same service (for example `_setHookId()` and `_getHookId()`) and move the
`hookId` read/write logic there, then update `activate()`/`deactivate()` to call
those helpers instead of touching `db` directly.

Source: Path instructions

},
async run(event) {
// Ack immediately so LoopQuest marks the delivery successful.
this.http.respond({ status: 200 });

const body = event.body;
if (!body || !body.task_id) return;

const verdict =
body.verdict === true ? "approved"
: body.verdict === false ? "flagged"
: body.escalated ? "escalated"
: "resolved";

this.$emit(body, {
id: body.task_id,
summary: `Verdict: ${verdict}${body.external_id ? ` (${body.external_id})` : ""}`,
ts: Date.now(),
});
Comment thread
coderabbitai[bot] marked this conversation as resolved.
},
};