-
Notifications
You must be signed in to change notification settings - Fork 0
Add HTTP Request and JS Script nodes with Cumulative State Inspector #4
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: main
Are you sure you want to change the base?
Changes from all commits
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 |
|---|---|---|
|
|
@@ -207,6 +207,37 @@ export function generatePython( | |
| lines.push(` print(json.dumps(payload, default=str, indent=2))`); | ||
| lines.push(` return payload`); | ||
| lines.push(``); | ||
| lines.push(`async def make_http_request(method: str, url: str, headers_json: str, body_str: str, state: State) -> Any:`); | ||
| lines.push(` import urllib.request`); | ||
| lines.push(` import urllib.error`); | ||
| lines.push(` log.info("HTTP %s %s", method, url)`); | ||
| lines.push(` headers = {}`); | ||
| lines.push(` if headers_json:`); | ||
| lines.push(` try:`); | ||
| lines.push(` headers = json.loads(headers_json)`); | ||
| lines.push(` except Exception:`); | ||
| lines.push(` pass`); | ||
| lines.push(` req_data = None`); | ||
| lines.push(` if body_str:`); | ||
| lines.push(` req_data = body_str.encode("utf-8")`); | ||
| lines.push(` if "Content-Type" not in headers and "content-type" not in headers:`); | ||
| lines.push(` headers["Content-Type"] = "application/json"`); | ||
| lines.push(` req = urllib.request.Request(url, data=req_data, headers=headers, method=method)`); | ||
| lines.push(` try:`); | ||
| lines.push(` with urllib.request.urlopen(req) as response:`); | ||
| lines.push(` status = response.status`); | ||
| lines.push(` resp_body = response.read().decode("utf-8")`); | ||
| lines.push(` try:`); | ||
| lines.push(` resp_body = json.loads(resp_body)`); | ||
| lines.push(` except Exception:`); | ||
| lines.push(` pass`); | ||
| lines.push(` return {"status": status, "body": resp_body}`); | ||
|
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. 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win Generated HTTP helpers omit response headers (Python and JS). Both
📍 Affects 1 file
🤖 Prompt for AI Agents |
||
| lines.push(` except urllib.error.HTTPError as err:`); | ||
| lines.push(` err_body = err.read().decode("utf-8")`); | ||
| lines.push(` raise RuntimeError(f"HTTP {err.code}: {err_body}")`); | ||
| lines.push(` except Exception as exc:`); | ||
| lines.push(` raise RuntimeError(f"HTTP request failed: {exc}")`); | ||
| lines.push(``); | ||
| lines.push(``); | ||
| lines.push(`# ---------------------------------------------------------------`); | ||
| lines.push(`# Graph definition (generated)`); | ||
|
|
@@ -309,6 +340,25 @@ export function generatePython( | |
| `await emit_output(${pyStr(c.target || "response")}, state)`, | ||
| `return "next"`, | ||
| ].join("\n"); | ||
| case "http": | ||
| return [ | ||
| `# HTTP Request`, | ||
| `method = ${pyStr(c.method || "GET")}`, | ||
| `url = ${pyStr(c.url || "")}`, | ||
| `headers_json = ${pyStr(c.headers || "")}`, | ||
| `body = ${pyStr(c.body || "")}`, | ||
| `result = await make_http_request(method, url, headers_json, body, state)`, | ||
| `state.last = result`, | ||
| `return "on_success"`, | ||
| ].join("\n"); | ||
| case "script": | ||
| return [ | ||
| `# JS Script Node (Simulated execution block)`, | ||
| `# Original Code:`, | ||
| ...((c.code || "").split("\n").map((line) => `# ${line}`)), | ||
| `# Setting execution stub outputs`, | ||
| `return "next"`, | ||
| ].join("\n"); | ||
| default: { | ||
| const _exhaustive: never = d.kind as never; | ||
| return `return "next" # unknown kind ${_exhaustive}`; | ||
|
|
@@ -342,6 +392,24 @@ export function generateJavaScript( | |
| lines.push(` get(k, d = null) { return this.data[k] ?? d; }`); | ||
| lines.push(`}`); | ||
| lines.push(``); | ||
| lines.push(`// adapters — swap with real SDKs`); | ||
| lines.push(`const makeHttpRequest = async (method, url, headersJson, body, state) => {`); | ||
| lines.push(` const headers = headersJson ? JSON.parse(headersJson) : {};`); | ||
| lines.push(` if (body && !headers["Content-Type"] && !headers["content-type"]) {`); | ||
| lines.push(` headers["Content-Type"] = "application/json";`); | ||
| lines.push(` }`); | ||
| lines.push(` const opts = { method, headers };`); | ||
| lines.push(` if (method !== "GET" && method !== "HEAD" && body) {`); | ||
| lines.push(` opts.body = body;`); | ||
| lines.push(` }`); | ||
| lines.push(` const res = await fetch(url, opts);`); | ||
| lines.push(` const text = await res.text();`); | ||
| lines.push(` let respBody;`); | ||
| lines.push(` try { respBody = JSON.parse(text); } catch { respBody = text; }`); | ||
| lines.push(` if (!res.ok) throw new Error(\`HTTP \${res.status}: \${text.slice(0, 100)}\`);`); | ||
| lines.push(` return { status: res.status, body: respBody };`); | ||
| lines.push(`};`); | ||
| lines.push(``); | ||
| lines.push(`class Graph {`); | ||
| lines.push(` constructor() { this.nodes = new Map(); this.edges = new Map(); this.entry = null; }`); | ||
| lines.push(` node(name, fn) { this.nodes.set(name, fn); if (!this.edges.has(name)) this.edges.set(name, []); }`); | ||
|
|
@@ -370,7 +438,6 @@ export function generateJavaScript( | |
| lines.push(` }`); | ||
| lines.push(`}`); | ||
| lines.push(``); | ||
| lines.push(`// adapters — swap with real SDKs`); | ||
| lines.push(`const callLlm = async (model, prompt) => \`[\${model}] \${prompt.slice(0, 40)}…\`;`); | ||
| lines.push(`const callTool = async (tool, args) => ({ tool, ok: true, args });`); | ||
| lines.push(`const memoryRead = async (key, state) => state.get(\`mem::\${key}\`);`); | ||
|
|
@@ -423,6 +490,10 @@ export function generateJavaScript( | |
| return `state.last = await awaitHuman(${JSON.stringify(c.channel || "ui")}, ${JSON.stringify(c.prompt || "")});\nreturn "next";`; | ||
| case "sink": | ||
| return `await emitOutput(${JSON.stringify(c.target || "response")}, state);\nreturn "next";`; | ||
| case "http": | ||
| return `const method = ${JSON.stringify(c.method || "GET")};\nconst url = ${JSON.stringify(c.url || "")};\nconst headers = ${JSON.stringify(c.headers || "")};\nconst body = ${JSON.stringify(c.body || "")};\nstate.last = await makeHttpRequest(method, url, headers, body, state);\nreturn "on_success";`; | ||
| case "script": | ||
| return `// JS Script Node Execution\n${c.code || ""}\nreturn "next";`; | ||
| default: | ||
| return `return "next";`; | ||
| } | ||
|
|
@@ -466,4 +537,4 @@ export function generateCode(lang: CodeLanguage, nodes: Node<AgentNodeData>[], e | |
| } | ||
|
|
||
| // also export the kind set for sanity | ||
| export const ALL_KINDS: AgentNodeKind[] = ["trigger","llm","tool","router","subagent","memory","human","sink"]; | ||
| export const ALL_KINDS: AgentNodeKind[] = ["trigger","llm","tool","router","subagent","memory","human","sink","http","script"]; | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -12,6 +12,8 @@ export interface RunLog { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||
| output?: unknown; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| error?: string; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ms: number; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** Snapshot of the execution state after this node runs */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| stateSnapshot?: Record<string, unknown>; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export interface RunOptions { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -78,8 +80,8 @@ function pickNextEdge( | |||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const onError = outgoing.find((e) => String(e.label) === "on_error"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (onError) return onError; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // router branch | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const branch = state.__router_branch as "true" | "false" | undefined; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // router or script branch override | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const branch = state.__router_branch as string | undefined; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (branch) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const m = outgoing.find((e) => String(e.label) === branch); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (m) return m; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -153,6 +155,7 @@ export async function runFlow(opts: RunOptions): Promise<RunLog[]> { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||
| output, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| error, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ms, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| stateSnapshot: JSON.parse(JSON.stringify(state)), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| logs.push(log); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| onLog?.(log); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -280,6 +283,87 @@ async function runNode( | |||||||||||||||||||||||||||||||||||||||||||||||||||||||
| result: state.last_output ?? null, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| case "http": { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const method = (cfg.method || "GET").trim().toUpperCase(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let urlStr = interpolate(cfg.url || "", state); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!urlStr.startsWith("http://") && !urlStr.startsWith("https://") && urlStr) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| urlStr = "https://" + urlStr; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!urlStr) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| throw new Error("HTTP request URL is required"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let parsedHeaders: Record<string, string> = {}; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (cfg.headers?.trim()) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const rawHeaders = interpolate(cfg.headers, state); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| parsedHeaders = JSON.parse(rawHeaders); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } catch { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| throw new Error("HTTP headers must be valid JSON"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let rawBody = interpolate(cfg.body || "", state); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Ensure content-type is set if body is JSON | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (rawBody && !parsedHeaders["Content-Type"] && !parsedHeaders["content-type"]) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| JSON.parse(rawBody); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| parsedHeaders["Content-Type"] = "application/json"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } catch { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // No-op, send raw | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const fetchOptions: RequestInit = { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| method, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| headers: parsedHeaders, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (method !== "GET" && method !== "HEAD" && rawBody) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| fetchOptions.body = rawBody; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const res = await fetch(urlStr, fetchOptions); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+317
to
+326
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. 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
There's no 🛡️ Proposed fix+ const controller = new AbortController();
+ const timeoutId = setTimeout(() => controller.abort(), 15000);
+ let res: Response;
+ try {
+ res = await fetch(urlStr, { ...fetchOptions, signal: controller.signal });
+ } finally {
+ clearTimeout(timeoutId);
+ }
- const res = await fetch(urlStr, fetchOptions);📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const text = await res.text(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let responseBody: unknown; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| responseBody = JSON.parse(text); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } catch { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| responseBody = text; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!res.ok) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| throw new Error(`HTTP ${res.status}: ${text.slice(0, 200)}`); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| status: res.status, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| headers: Object.fromEntries(res.headers.entries()), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| body: responseBody, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| case "script": { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const userCode = cfg.code || ""; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Create a sandboxed execution where the user can read/write properties on `state` | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // We will allow returning a value which gets mapped as custom edge label. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const fn = new Function("state", ` | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| with (state) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ${userCode} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| `); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // We evaluate using a Proxy or just normal state object. To make standard assignments work, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // we can let standard state mutation happen directly on the state reference. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const returnedLabel = fn(state); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (typeof returnedLabel === "string") { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| state.__router_branch = returnedLabel; // Re-use the branch selector matching logic | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return { returned: returnedLabel, success: true }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return { success: true }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } catch (e) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| throw new Error(`Script evaluation error: ${e instanceof Error ? e.message : String(e)}`); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+345
to
+366
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. 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
# Inspect the relevant file range and nearby helpers.
FILE="frontend/src/flow/runFlow.ts"
wc -l "$FILE"
sed -n '300,410p' "$FILE"
# Search for related script execution / state handling.
rg -n "new Function|with \\(state\\)|__router_branch|script" frontend/src/flow/runFlow.ts frontend/src/flow -S
# Probe JavaScript semantics relevant to the comment.
node - <<'JS'
function run(userCode, state) {
const fn = new Function("state", `
with (state) {
${userCode}
}
`);
return fn(state);
}
const state = {};
try {
run("newVar = 5; return typeof newVar + ':' + state.newVar;", state);
console.log("state after assignment:", JSON.stringify(state));
console.log("global newVar:", globalThis.newVar);
delete globalThis.newVar;
} catch (e) {
console.error("error:", e && e.message);
}
const state2 = {};
try {
const out = run("state.x = 1; return x + ':' + state.x;", state2);
console.log("output for state.x assignment:", out);
} catch (e) {
console.error("error2:", e && e.message);
}
const state3 = { existing: 0 };
try {
const out = run("existing = 7; return existing + ':' + state.existing;", state3);
console.log("output for existing property assignment:", out);
console.log("state3 after:", JSON.stringify(state3));
} catch (e) {
console.error("error3:", e && e.message);
}
JSRepository: Jacobcdsmith/agent-flow-canvas Length of output: 2059
🧰 Tools🪛 OpenGrep (1.25.0)[ERROR] 350-354: new Function() with dynamic input can execute arbitrary code. Avoid dynamic code evaluation entirely, or use a safe alternative. (coderabbit.code-injection.new-function-js) 🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| default: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return { kind: node.data.kind, note: "no executor" }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Inconsistent header-JSON error handling across generated helpers. The Python helper silently ignores invalid header JSON (
except Exception: pass) while the JS helper throws a rawSyntaxErrorfromJSON.parse; the runtime throws a descriptive"HTTP headers must be valid JSON"error. Both generated helpers should match the runtime's fail-fast behavior.frontend/src/flow/codegen.ts#L215-L219: Replacepasswithraise RuntimeError(f"HTTP headers must be valid JSON: {exc}").frontend/src/flow/codegen.ts#L397: WrapJSON.parse(headersJson)in try/catch and thrownew Error("HTTP headers must be valid JSON")on failure.📍 Affects 1 file
frontend/src/flow/codegen.ts#L215-L219(this comment)frontend/src/flow/codegen.ts#L397-L397🤖 Prompt for AI Agents