-
Notifications
You must be signed in to change notification settings - Fork 5.7k
New Components - loopquest #21304
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
New Components - loopquest #21304
Changes from all commits
d900bd1
c0ee310
877113f
7f67f63
561cdb6
a0f73d8
6c16e47
250fd22
510c357
75760e7
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,50 @@ | ||
| 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 documentation](https://loopquest.tomphillips.uk/docs).", | ||
| version: "0.0.1", | ||
| type: "action", | ||
| annotations: { | ||
| readOnlyHint: false, | ||
| destructiveHint: false, | ||
| openWorldHint: true, | ||
| }, | ||
| 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; | ||
| }, | ||
| }; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,23 @@ | ||
| 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 documentation](https://loopquest.tomphillips.uk/docs).", | ||
| version: "0.0.1", | ||
| type: "action", | ||
| annotations: { | ||
| readOnlyHint: true, | ||
| destructiveHint: false, | ||
| openWorldHint: true, | ||
| }, | ||
| 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; | ||
| }, | ||
| }; | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
| 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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 Since these values are sent via ♻️ 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
Suggested change
🤖 Prompt for AI AgentsSource: Coding guidelines |
||||||||||||||||||||||||||
| return body; | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,79 @@ | ||
| 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, description: "Optional heading shown on the review card." }, | ||
| 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, description: "How many people must review before the verdict resolves. Defaults to 1." }, | ||
| taskId: { type: "string", label: "Task ID", description: "The task id returned by **Create Review Task** — used by **Get Task Status**." }, | ||
| }, | ||
| methods: { | ||
| _baseUrl() { | ||
| return (this.$auth.base_url || "https://loopquest.tomphillips.uk").replace(/\/+$/, ""); | ||
| }, | ||
| _headers() { | ||
| return { authorization: `Bearer ${this.$auth.api_key}`, "content-type": "application/json" }; | ||
| }, | ||
| // Single place that constructs the axios call — every method delegates here. | ||
| _makeRequest({ $, path, ...opts }) { | ||
| return axios($, { | ||
| url: `${this._baseUrl()}${path}`, | ||
| headers: this._headers(), | ||
| ...opts, | ||
| }); | ||
| }, | ||
| async createTask({ $, props }) { | ||
| return this._makeRequest({ $, method: "POST", path: "/api/v1/tasks", data: buildTaskBody(props) }); | ||
| }, | ||
| async getTask({ $, taskId }) { | ||
| return this._makeRequest({ $, method: "GET", path: `/api/v1/tasks/${taskId}` }); | ||
| }, | ||
| // Verdict subscriptions — power the New Verdict source (REST-hook style). | ||
| async subscribeVerdicts({ $, url }) { | ||
| return this._makeRequest({ $, method: "POST", path: "/api/v1/hooks", data: { url } }); | ||
| }, | ||
| async unsubscribeVerdicts({ $, id }) { | ||
| return this._makeRequest({ $, method: "DELETE", path: `/api/v1/hooks/${id}` }); | ||
| }, | ||
| }, | ||
| }; |
| 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" | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,48 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||
| 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 documentation](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, url: this.http.endpoint }); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| this.db.set("hookId", id); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| async deactivate() { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| const hookId = this.db.get("hookId"); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| if (hookId) await this.loopquest.unsubscribeVerdicts({ $: this, id: hookId }); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+18
to
+25
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win Encapsulate
♻️ 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 📝 Committable suggestion
Suggested change
🤖 Prompt for AI AgentsSource: 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, { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| // Unique per delivery: a task's state (approved/flagged/escalated) makes | ||||||||||||||||||||||||||||||||||||||||||||||||||
| // the id stable for the same resolution but distinct if state ever differs. | ||||||||||||||||||||||||||||||||||||||||||||||||||
| id: `${body.task_id}:${verdict}`, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| summary: `Verdict: ${verdict}${body.external_id ? ` (${body.external_id})` : ""}`, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| ts: Date.now(), | ||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
coderabbitai[bot] marked this conversation as resolved.
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||
Uh oh!
There was an error while loading. Please reload this page.