Skip to content

Commit

Permalink
feat: Print URL to link to execution. (#293)
Browse files Browse the repository at this point in the history
## Proposed changes

If `printUrl` is set when running the workflow, the checkpointer prints
the URL to access the execution in the console.

Unfortunately, due to the background nature of checkpointing, there is
no way for us to ensure that this outputs first.

```
pnpm start

> @gensx-examples/hacker-news-analyzer@0.0.0 start /Users/jeremy/code/cortexclick/gensx/examples/hackerNewsAnalyzer
> NODE_OPTIONS='--enable-source-maps' tsx ./index.ts


🚀 Starting HN analysis workflow...\
(node:43650) [DEP0040] DeprecationWarning: The `punycode` module is deprecated. Please use a userland alternative instead.
(Use `node --trace-deprecation ...` to show where the warning was created)
📚 Collecting up to 500 HN posts (text posts only)...


[GenSX] View execution at: http://localhost:3000/gensx/workflows/AnalyzeHackerNewsWorkflow/01JMGFCP7VEQ2KFZPFDTBSVGZS


📝 Found 43 text posts out of 500 total posts 
⚠️  Note: Requested 500 posts but only found 43 text posts in the top 500 stories
✅ Analysis complete! Check hn_analysis_report.md and hn_analysis_tweet.txt
```
  • Loading branch information
jmoseley authored Feb 20, 2025
1 parent 8ce57d8 commit 3f6898e
Show file tree
Hide file tree
Showing 17 changed files with 178 additions and 77 deletions.
11 changes: 7 additions & 4 deletions examples/blogWriter/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,13 @@ import { WriteBlog } from "./writeBlog.js";
async function main() {
console.log("\n🚀 Starting blog writing workflow");
const wf = gsx.Workflow("WriteBlogWorkflow", WriteBlog);
const stream = await wf.run({
stream: true,
prompt: "Write a blog post about the future of AI",
});
const stream = await wf.run(
{
stream: true,
prompt: "Write a blog post about the future of AI",
},
{ printUrl: true },
);
for await (const chunk of stream) {
process.stdout.write(chunk);
}
Expand Down
2 changes: 1 addition & 1 deletion examples/contexts/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ async function main() {
// Provide a value to the context
const result = await gsx
.Workflow("ContextExampleWorkflow", ContextExample)
.run({});
.run({}, { printUrl: true });
console.log(result);
}

Expand Down
9 changes: 6 additions & 3 deletions examples/deepResearch/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,12 @@ async function main() {
const prompt =
"find research comparing the writing style of humans and LLMs. We want to figure out how to quantify the differences.";
console.log("\n🚀 Starting deep research workflow with prompt: ", prompt);
const result = await gsx.Workflow("DeepResearchWorkflow", DeepResearch).run({
prompt,
});
const result = await gsx.Workflow("DeepResearchWorkflow", DeepResearch).run(
{
prompt,
},
{ printUrl: true },
);

console.log("\n\n");
console.log("=".repeat(50));
Expand Down
12 changes: 6 additions & 6 deletions examples/gsxChatCompletion/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ function basicCompletion() {
BasicCompletionExample,
);

return workflow.run({});
return workflow.run({}, { printUrl: true });
}

function tools() {
Expand Down Expand Up @@ -93,7 +93,7 @@ function tools() {

const workflow = gsx.Workflow("ToolsExampleWorkflow", ToolsExample);

return workflow.run({});
return workflow.run({}, { printUrl: true });
}

function toolsStreaming() {
Expand Down Expand Up @@ -149,7 +149,7 @@ function toolsStreaming() {
ToolsStreamingExample,
);

return workflow.run({});
return workflow.run({}, { printUrl: true });
}

function streamingCompletion() {
Expand Down Expand Up @@ -182,7 +182,7 @@ function streamingCompletion() {
StreamingCompletionWorkflow,
);

return workflow.run({});
return workflow.run({}, { printUrl: true });
}

function structuredOutput() {
Expand Down Expand Up @@ -235,7 +235,7 @@ function structuredOutput() {
StructuredOutputWorkflow,
);

return workflow.run({});
return workflow.run({}, { printUrl: true });
}

function multiStepTools() {
Expand Down Expand Up @@ -320,7 +320,7 @@ Please explain your thinking as you go through this analysis.`,
MultiStepToolsWorkflow,
);

return workflow.run({});
return workflow.run({}, { printUrl: true });
}

async function main() {
Expand Down
9 changes: 6 additions & 3 deletions examples/hackerNewsAnalyzer/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,12 @@ async function main() {
console.log("\n🚀 Starting HN analysis workflow...");
const { report, tweet } = await gsx
.Workflow("AnalyzeHackerNewsWorkflow", AnalyzeHackerNewsTrends)
.run({
postCount: 500,
});
.run(
{
postCount: 500,
},
{ printUrl: true },
);

// Write outputs to files
await fs.writeFile("hn_analysis_report.md", report);
Expand Down
9 changes: 6 additions & 3 deletions examples/providers/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,12 @@ async function main() {
const workflow = gsx.Workflow("ScrapePageExampleWorkflow", ScrapePageExample);

console.log("\n🚀 Scraping page from url:", url);
const markdown = await workflow.run({
url,
});
const markdown = await workflow.run(
{
url,
},
{ printUrl: true },
);
console.log("\n✅ Scraping complete");
console.log("\n🚀 Scraped markdown:", markdown);
}
Expand Down
9 changes: 6 additions & 3 deletions examples/reflection/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -83,12 +83,15 @@ async function main() {
"CleanBuzzwordsWorkflow",
CleanBuzzwordsReflectionLoop,
);
const withoutBuzzwords = await workflow.run({
text: `We are a cutting-edge technology company leveraging bleeding-edge AI solutions to deliver best-in-class products to our customers. Our agile development methodology ensures we stay ahead of the curve with paradigm-shifting innovations.
const withoutBuzzwords = await workflow.run(
{
text: `We are a cutting-edge technology company leveraging bleeding-edge AI solutions to deliver best-in-class products to our customers. Our agile development methodology ensures we stay ahead of the curve with paradigm-shifting innovations.
Our mission-critical systems utilize cloud-native architectures and next-generation frameworks to create synergistic solutions that drive digital transformation. By thinking outside the box, we empower stakeholders with scalable and future-proof applications that maximize ROI.
Through our holistic approach to disruptive innovation, we create game-changing solutions that move the needle and generate impactful results. Our best-of-breed technology stack combined with our customer-centric focus allows us to ideate and iterate rapidly in this fast-paced market.`,
});
},
{ printUrl: true },
);

console.log("result:\n", withoutBuzzwords);
}
Expand Down
10 changes: 8 additions & 2 deletions examples/streaming/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ async function runStreamingWithChildrenExample() {
const workflow = gsx.Workflow(
"StreamingStoryWithChildrenWorkflow",
StreamStoryWithChildren,
{ printUrl: true },
);

console.log("\n📝 Non-streaming version (waiting for full response):");
Expand All @@ -122,14 +123,19 @@ async function runStreamingExample() {

console.log("\n🚀 Starting streaming example with prompt:", prompt);

const workflow = gsx.Workflow("StreamStoryWorkflow", StreamStory);
const workflow = gsx.Workflow("StreamStoryWorkflow", StreamStory, {
printUrl: true,
});

console.log("\n📝 Non-streaming version (waiting for full response):");
const finalResult = await workflow.run({ prompt });
console.log("✅ Complete response:", finalResult);

console.log("\n📝 Streaming version (processing tokens as they arrive):");
const response = await workflow.run({ prompt, stream: true });
const response = await workflow.run(
{ prompt, stream: true },
{ printUrl: true },
);

for await (const token of response) {
process.stdout.write(token);
Expand Down
18 changes: 12 additions & 6 deletions examples/structuredOutputs/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -92,19 +92,25 @@ async function main() {

console.log("\n🎯 Getting structured outputs with GSXChatCompletion");
const workflow = gsx.Workflow("ExtractEntities", ExtractEntities);
const result = await workflow.run({
text: "John Doe is a software engineer at Google.",
});
const result = await workflow.run(
{
text: "John Doe is a software engineer at Google.",
},
{ printUrl: true },
);
console.log(result);

console.log("\n🎯 Getting structured outputs without helpers");
const workflowWithoutHelpers = gsx.Workflow(
"ExtractEntitiesWithoutHelpers",
ExtractEntitiesWithoutHelpers,
);
const resultWithoutHelpers = await workflowWithoutHelpers.run({
text: "John Doe is a software engineer at Google.",
});
const resultWithoutHelpers = await workflowWithoutHelpers.run(
{
text: "John Doe is a software engineer at Google.",
},
{ printUrl: true },
);
console.log(resultWithoutHelpers);
console.log("\n✅ Structured outputs example complete");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,11 @@ const WorkflowComponent = gsx.Component<{ userInput: string }, string>(

const workflow = gsx.Workflow("MyGSXWorkflow", WorkflowComponent);

const result = await workflow.run({
userInput: "Hi there! Say 'Hello, World!' and nothing else.",
});
const result = await workflow.run(
{
userInput: "Hi there! Say 'Hello, World!' and nothing else.",
},
{ printUrl: true },
);

console.log(result);
3 changes: 3 additions & 0 deletions packages/gensx-cli/src/commands/login.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,9 @@ async function saveConfig(config: Config): Promise<void> {
org: config.orgSlug,
baseUrl: API_BASE_URL,
},
console: {
baseUrl: APP_BASE_URL,
},
});

// Add a helpful header comment
Expand Down
60 changes: 42 additions & 18 deletions packages/gensx/src/checkpoint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,9 @@ export class CheckpointManager implements CheckpointWriter {
private version = 1;
private org: string;
private apiKey: string;
private baseUrl: string;
private apiBaseUrl: string;
private consoleBaseUrl: string;
private printUrl = false;

// Provide unified view of all secrets
get secretValues(): Set<unknown> {
Expand All @@ -84,20 +86,29 @@ export class CheckpointManager implements CheckpointWriter {
apiKey: string;
org: string;
disabled?: boolean;
baseUrl?: string;
apiBaseUrl?: string;
consoleBaseUrl?: string;
}) {
// Priority order: constructor opts > env vars > config file
const config = readConfig();
const apiKey =
opts?.apiKey ?? process.env.GENSX_API_KEY ?? config.api?.token;
const org = opts?.org ?? process.env.GENSX_ORG ?? config.api?.org;
const baseUrl =
opts?.baseUrl ?? process.env.GENSX_CHECKPOINT_URL ?? config.api?.baseUrl;
const apiBaseUrl =
opts?.apiBaseUrl ??
process.env.GENSX_CHECKPOINT_URL ??
config.api?.baseUrl;
const consoleBaseUrl =
opts?.consoleBaseUrl ??
process.env.GENSX_CONSOLE_URL ??
config.console?.baseUrl;

this.checkpointsEnabled = apiKey !== undefined;
this.org = org ?? "";
this.apiKey = apiKey ?? "";
this.baseUrl = baseUrl ?? "https://api.gensx.com";
this.apiBaseUrl = apiBaseUrl ?? "https://api.gensx.com";
this.consoleBaseUrl = consoleBaseUrl ?? "https://app.gensx.com";

if (
opts?.disabled ||
process.env.GENSX_CHECKPOINTS === "false" ||
Expand Down Expand Up @@ -230,6 +241,7 @@ export class CheckpointManager implements CheckpointWriter {
});
}

private havePrintedUrl = false;
private async writeCheckpoint() {
if (!this.root) return;

Expand All @@ -255,7 +267,7 @@ export class CheckpointManager implements CheckpointWriter {

const treeCopy = cloneWithoutFunctions(this.root);
const maskedRoot = this.maskExecutionTree(treeCopy as ExecutionNode);
const url = join(this.baseUrl, `/org/${this.org}/executions`);
const url = join(this.apiBaseUrl, `/org/${this.org}/executions`);
const steps = this.countSteps(this.root);

// Separately gzip the rawExecution data
Expand All @@ -271,11 +283,12 @@ export class CheckpointManager implements CheckpointWriter {
const base64CompressedExecution =
Buffer.from(compressedExecution).toString("base64");

const workflowName = this.workflowName ?? this.root.componentName;
const payload = {
executionId: this.root.id,
version: this.version,
schemaVersion: 2,
workflowName: this.root.componentName,
workflowName,
startedAt: this.root.startTime,
completedAt: this.root.endTime,
rawExecution: base64CompressedExecution,
Expand All @@ -301,6 +314,24 @@ export class CheckpointManager implements CheckpointWriter {
message: await response.text(),
});
}

if (this.printUrl && !this.havePrintedUrl && response.ok) {
const responseBody = (await response.json()) as {
status: "ok";
data: {
executionId: string;
workflowName?: string;
};
};
const executionUrl = new URL(
`/${this.org}/workflows/${responseBody.data.workflowName ?? workflowName}/${responseBody.data.executionId}`,
this.consoleBaseUrl,
);
this.havePrintedUrl = true;
console.info(
`\n\n\x1b[33m[GenSX] View execution at:\x1b[0m \x1b[1;34m${executionUrl.toString()}\x1b[0m\n\n`,
);
}
} catch (error) {
console.error(`[Checkpoint] Failed to save checkpoint:`, { error });
}
Expand Down Expand Up @@ -599,10 +630,6 @@ export class CheckpointManager implements CheckpointWriter {
// Handle root node case
if (!this.root) {
this.root = node;
// If the workflow name is set, update the root node name.
if (this.workflowName) {
this.root.componentName = this.workflowName;
}
} else if (this.root.parentId === node.id) {
// Current root was waiting for this node as parent
this.attachToParent(this.root, node);
Expand Down Expand Up @@ -666,13 +693,13 @@ export class CheckpointManager implements CheckpointWriter {
}
}

// TODO: What if we have already sent some checkpoints?
setWorkflowName(name: string) {
// Right now we just update the name of the root node. Eventually this should be separated from the workflow name.
this.workflowName = name;
}

if (this.root) {
this.root.componentName = name;
}
setPrintUrl(printUrl: boolean) {
this.printUrl = printUrl;
}

updateNode(id: string, updates: Partial<ExecutionNode>) {
Expand All @@ -694,9 +721,6 @@ export class CheckpointManager implements CheckpointWriter {
}

Object.assign(node, updates);
if (node.id === this.root?.id) {
node.componentName = this.workflowName ?? node.componentName;
}
this.updateCheckpoint();
} else {
console.warn(`[Tracker] Attempted to update unknown node:`, { id });
Expand Down
3 changes: 3 additions & 0 deletions packages/gensx/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ export interface GensxConfig {
org?: string;
baseUrl?: string;
};
console?: {
baseUrl?: string;
};
}

export function getConfigPath(): string {
Expand Down
Loading

0 comments on commit 3f6898e

Please sign in to comment.