flowcraft 1.0.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (148) hide show
  1. package/.editorconfig +9 -0
  2. package/LICENSE +21 -0
  3. package/README.md +249 -0
  4. package/config/tsconfig.json +21 -0
  5. package/config/tsup.config.ts +11 -0
  6. package/config/vitest.config.ts +11 -0
  7. package/docs/.vitepress/config.ts +105 -0
  8. package/docs/api-reference/builder.md +158 -0
  9. package/docs/api-reference/fn.md +142 -0
  10. package/docs/api-reference/index.md +38 -0
  11. package/docs/api-reference/workflow.md +126 -0
  12. package/docs/guide/advanced-guides/cancellation.md +117 -0
  13. package/docs/guide/advanced-guides/composition.md +68 -0
  14. package/docs/guide/advanced-guides/custom-executor.md +180 -0
  15. package/docs/guide/advanced-guides/error-handling.md +135 -0
  16. package/docs/guide/advanced-guides/logging.md +106 -0
  17. package/docs/guide/advanced-guides/middleware.md +106 -0
  18. package/docs/guide/advanced-guides/observability.md +175 -0
  19. package/docs/guide/best-practices/debugging.md +182 -0
  20. package/docs/guide/best-practices/state-management.md +120 -0
  21. package/docs/guide/best-practices/sub-workflow-data.md +95 -0
  22. package/docs/guide/best-practices/testing.md +187 -0
  23. package/docs/guide/builders.md +157 -0
  24. package/docs/guide/functional-api.md +133 -0
  25. package/docs/guide/index.md +178 -0
  26. package/docs/guide/recipes/creating-a-loop.md +113 -0
  27. package/docs/guide/recipes/data-processing-pipeline.md +123 -0
  28. package/docs/guide/recipes/fan-out-fan-in.md +112 -0
  29. package/docs/guide/recipes/index.md +15 -0
  30. package/docs/guide/recipes/resilient-api-call.md +110 -0
  31. package/docs/guide/tooling/graph-validation.md +160 -0
  32. package/docs/guide/tooling/mermaid.md +156 -0
  33. package/docs/index.md +56 -0
  34. package/eslint.config.js +16 -0
  35. package/package.json +40 -0
  36. package/pnpm-workspace.yaml +2 -0
  37. package/sandbox/1.basic/README.md +45 -0
  38. package/sandbox/1.basic/package.json +16 -0
  39. package/sandbox/1.basic/src/flow.ts +17 -0
  40. package/sandbox/1.basic/src/main.ts +22 -0
  41. package/sandbox/1.basic/src/nodes.ts +112 -0
  42. package/sandbox/1.basic/src/utils.ts +35 -0
  43. package/sandbox/1.basic/tsconfig.json +3 -0
  44. package/sandbox/2.research/README.md +46 -0
  45. package/sandbox/2.research/package.json +16 -0
  46. package/sandbox/2.research/src/flow.ts +14 -0
  47. package/sandbox/2.research/src/main.ts +31 -0
  48. package/sandbox/2.research/src/nodes.ts +108 -0
  49. package/sandbox/2.research/src/utils.ts +45 -0
  50. package/sandbox/2.research/src/visualize.ts +29 -0
  51. package/sandbox/2.research/tsconfig.json +3 -0
  52. package/sandbox/3.parallel/README.md +65 -0
  53. package/sandbox/3.parallel/package.json +16 -0
  54. package/sandbox/3.parallel/src/main.ts +45 -0
  55. package/sandbox/3.parallel/src/nodes.ts +43 -0
  56. package/sandbox/3.parallel/src/utils.ts +25 -0
  57. package/sandbox/3.parallel/tsconfig.json +3 -0
  58. package/sandbox/4.dag/README.md +179 -0
  59. package/sandbox/4.dag/data/1.blog-post/100.json +60 -0
  60. package/sandbox/4.dag/data/1.blog-post/README.md +25 -0
  61. package/sandbox/4.dag/data/2.job-application/200.json +103 -0
  62. package/sandbox/4.dag/data/2.job-application/201.json +31 -0
  63. package/sandbox/4.dag/data/2.job-application/202.json +31 -0
  64. package/sandbox/4.dag/data/2.job-application/README.md +58 -0
  65. package/sandbox/4.dag/data/3.customer-review/300.json +141 -0
  66. package/sandbox/4.dag/data/3.customer-review/301.json +31 -0
  67. package/sandbox/4.dag/data/3.customer-review/302.json +28 -0
  68. package/sandbox/4.dag/data/3.customer-review/README.md +71 -0
  69. package/sandbox/4.dag/data/4.content-moderation/400.json +161 -0
  70. package/sandbox/4.dag/data/4.content-moderation/401.json +47 -0
  71. package/sandbox/4.dag/data/4.content-moderation/402.json +46 -0
  72. package/sandbox/4.dag/data/4.content-moderation/403.json +31 -0
  73. package/sandbox/4.dag/data/4.content-moderation/README.md +83 -0
  74. package/sandbox/4.dag/package.json +19 -0
  75. package/sandbox/4.dag/src/main.ts +73 -0
  76. package/sandbox/4.dag/src/nodes.ts +134 -0
  77. package/sandbox/4.dag/src/registry.ts +87 -0
  78. package/sandbox/4.dag/src/types.ts +25 -0
  79. package/sandbox/4.dag/src/utils.ts +42 -0
  80. package/sandbox/4.dag/tsconfig.json +3 -0
  81. package/sandbox/5.distributed/.env.example +1 -0
  82. package/sandbox/5.distributed/README.md +88 -0
  83. package/sandbox/5.distributed/data/1.blog-post/100.json +59 -0
  84. package/sandbox/5.distributed/data/1.blog-post/README.md +25 -0
  85. package/sandbox/5.distributed/data/2.job-application/200.json +103 -0
  86. package/sandbox/5.distributed/data/2.job-application/201.json +30 -0
  87. package/sandbox/5.distributed/data/2.job-application/202.json +30 -0
  88. package/sandbox/5.distributed/data/2.job-application/README.md +58 -0
  89. package/sandbox/5.distributed/data/3.customer-review/300.json +141 -0
  90. package/sandbox/5.distributed/data/3.customer-review/301.json +31 -0
  91. package/sandbox/5.distributed/data/3.customer-review/302.json +57 -0
  92. package/sandbox/5.distributed/data/3.customer-review/README.md +71 -0
  93. package/sandbox/5.distributed/data/4.content-moderation/400.json +173 -0
  94. package/sandbox/5.distributed/data/4.content-moderation/401.json +47 -0
  95. package/sandbox/5.distributed/data/4.content-moderation/402.json +46 -0
  96. package/sandbox/5.distributed/data/4.content-moderation/403.json +31 -0
  97. package/sandbox/5.distributed/data/4.content-moderation/README.md +83 -0
  98. package/sandbox/5.distributed/package.json +20 -0
  99. package/sandbox/5.distributed/src/client.ts +124 -0
  100. package/sandbox/5.distributed/src/executor.ts +69 -0
  101. package/sandbox/5.distributed/src/nodes.ts +136 -0
  102. package/sandbox/5.distributed/src/registry.ts +101 -0
  103. package/sandbox/5.distributed/src/types.ts +45 -0
  104. package/sandbox/5.distributed/src/utils.ts +69 -0
  105. package/sandbox/5.distributed/src/worker.ts +217 -0
  106. package/sandbox/5.distributed/tsconfig.json +3 -0
  107. package/sandbox/6.rag/.env.example +1 -0
  108. package/sandbox/6.rag/README.md +60 -0
  109. package/sandbox/6.rag/data/README.md +31 -0
  110. package/sandbox/6.rag/data/rag.json +58 -0
  111. package/sandbox/6.rag/documents/sample-cascade.txt +11 -0
  112. package/sandbox/6.rag/package.json +18 -0
  113. package/sandbox/6.rag/src/main.ts +52 -0
  114. package/sandbox/6.rag/src/nodes/GenerateEmbeddingsNode.ts +54 -0
  115. package/sandbox/6.rag/src/nodes/LLMProcessNode.ts +48 -0
  116. package/sandbox/6.rag/src/nodes/LoadAndChunkNode.ts +40 -0
  117. package/sandbox/6.rag/src/nodes/StoreInVectorDBNode.ts +36 -0
  118. package/sandbox/6.rag/src/nodes/VectorSearchNode.ts +53 -0
  119. package/sandbox/6.rag/src/nodes/index.ts +28 -0
  120. package/sandbox/6.rag/src/registry.ts +23 -0
  121. package/sandbox/6.rag/src/types.ts +44 -0
  122. package/sandbox/6.rag/src/utils.ts +77 -0
  123. package/sandbox/6.rag/tsconfig.json +3 -0
  124. package/sandbox/tsconfig.json +13 -0
  125. package/src/builder/collection.test.ts +287 -0
  126. package/src/builder/collection.ts +269 -0
  127. package/src/builder/graph.test.ts +406 -0
  128. package/src/builder/graph.ts +336 -0
  129. package/src/builder/graph.types.ts +104 -0
  130. package/src/builder/index.ts +3 -0
  131. package/src/context.ts +111 -0
  132. package/src/errors.ts +34 -0
  133. package/src/executor.ts +29 -0
  134. package/src/executors/in-memory.test.ts +93 -0
  135. package/src/executors/in-memory.ts +140 -0
  136. package/src/functions.test.ts +191 -0
  137. package/src/functions.ts +117 -0
  138. package/src/index.ts +5 -0
  139. package/src/logger.ts +41 -0
  140. package/src/types.ts +75 -0
  141. package/src/utils/graph.test.ts +144 -0
  142. package/src/utils/graph.ts +182 -0
  143. package/src/utils/index.ts +3 -0
  144. package/src/utils/mermaid.test.ts +239 -0
  145. package/src/utils/mermaid.ts +133 -0
  146. package/src/utils/sleep.ts +20 -0
  147. package/src/workflow.test.ts +622 -0
  148. package/src/workflow.ts +561 -0
package/docs/index.md ADDED
@@ -0,0 +1,56 @@
1
+ ---
2
+ layout: home
3
+
4
+ hero:
5
+ name: Flowcraft
6
+ text: A Workflow Framework
7
+ tagline: Build complex, multi-step processes, from simple sequences to dynamic, graph-driven AI agents.
8
+ actions:
9
+ - theme: brand
10
+ text: Introduction
11
+ link: /guide/
12
+ - theme: alt
13
+ text: Recipes
14
+ link: /guide/recipes/
15
+ - theme: alt
16
+ text: Sandbox
17
+ link: https://github.com/gorango/flowcraft/tree/master/sandbox
18
+
19
+ features:
20
+ - title: Zero Dependencies
21
+ icon: 🌱
22
+ details: Lightweight and dependency-free, ensuring a small footprint and easy integration.
23
+ - title: Composable & Reusable
24
+ icon: 🧩
25
+ details: Define workflows by chaining nodes or embedding other flows as nodes.
26
+ - title: Extensible Execution Engine
27
+ icon: šŸ”Œ
28
+ details: A pluggable Executor pattern enables in-memory or distributed flows.
29
+ # - title: Type-Safe
30
+ # icon: šŸ›”ļø
31
+ # details: Written in TypeScript to provide strong typing for your workflow definitions and context.
32
+ # - title: Async by Default
33
+ # icon: ⚔
34
+ # details: Built on an asynchronous foundation to handle I/O-bound and CPU-bound tasks.
35
+ - title: Middleware
36
+ icon: 🄪
37
+ details: Intercept execution of nodes to handle cross-cutting concerns like logging, timing, or auth.
38
+ # - title: Conditional Branching
39
+ # icon: šŸ”€
40
+ # details: Direct the flow's execution path based on the results of any node.
41
+ # - title: Retry Logic & Fallbacks
42
+ # icon: šŸ”„
43
+ # details: Retry failed operations with configurable delays and fallback logic.
44
+ - title: Cancellation
45
+ icon: šŸ›‘
46
+ details: Gracefully abort in-progress workflows using standard AbortControllers.
47
+ # - title: Pluggable Logging
48
+ # icon: šŸ“
49
+ # details: Use the built-in ConsoleLogger or bring your own (e.g., Pino, Winston).
50
+ - title: Dynamic Graph Engine
51
+ icon: 🌐
52
+ details: Define complex, graph-based workflows as simple JSON files.
53
+ # - title: Fluent & Functional API
54
+ # icon: ✨
55
+ # details: A chainable API on the Node class and a collection of functional helpers.
56
+ ---
@@ -0,0 +1,16 @@
1
+ import antfu from '@antfu/eslint-config'
2
+
3
+ export default antfu({
4
+ stylistic: {
5
+ indent: 'tab',
6
+ quotes: 'single',
7
+ semi: false,
8
+ },
9
+ }, {
10
+ rules: {
11
+ 'no-console': 'off',
12
+ 'unused-imports/no-unused-vars': 'off',
13
+ 'unused-imports/no-unused-imports': 'off',
14
+ 'ts/no-this-alias': 'off',
15
+ },
16
+ })
package/package.json ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "flowcraft",
3
+ "type": "module",
4
+ "version": "1.0.0-beta.1",
5
+ "packageManager": "pnpm@10.13.1",
6
+ "description": "A flexible workflow framework",
7
+ "exports": {
8
+ ".": {
9
+ "types": "./dist/index.d.ts",
10
+ "import": "./dist/index.js"
11
+ }
12
+ },
13
+ "engines": {
14
+ "node": ">=24",
15
+ "pnpm": ">=10"
16
+ },
17
+ "scripts": {
18
+ "dev": "tsup --watch --config config/tsup.config.ts",
19
+ "build": "tsup --config config/tsup.config.ts",
20
+ "test": "vitest run --config config/vitest.config.ts",
21
+ "test:watch": "vitest --config config/vitest.config.ts",
22
+ "coverage": "vitest run --coverage --config config/vitest.config.ts",
23
+ "lint": "eslint --fix . --ext .ts --config eslint.config.js",
24
+ "docs:dev": "vitepress dev docs",
25
+ "docs:build": "vitepress build docs",
26
+ "docs:preview": "vitepress preview docs"
27
+ },
28
+ "devDependencies": {
29
+ "@antfu/eslint-config": "^4.16.2",
30
+ "@types/node": "^24.0.13",
31
+ "@vitest/coverage-v8": "3.2.4",
32
+ "eslint": "^9.31.0",
33
+ "mermaid": "^11.9.0",
34
+ "tsup": "^8.5.0",
35
+ "typescript": "^5.8.3",
36
+ "vitepress": "^1.6.3",
37
+ "vitepress-plugin-mermaid": "^2.0.17",
38
+ "vitest": "^3.2.4"
39
+ }
40
+ }
@@ -0,0 +1,2 @@
1
+ packages:
2
+ - 'sandbox/*'
@@ -0,0 +1,45 @@
1
+ # Article Writing Workflow
2
+
3
+ A `workflow` example that demonstrates an article writing workflow using a sequence of `Node`s and `Flow`.
4
+
5
+ ## Features
6
+
7
+ - **Generate Outline**: Creates a simple outline with up to 3 main sections using YAML structured output.
8
+ - **Write Content**: Uses a `BatchNode`-like pattern within a single Node to write concise content for each section.
9
+ - **Apply Style**: Applies a conversational, engaging style to the final article.
10
+
11
+ ## How to Run
12
+
13
+ 1. **Install dependencies**:
14
+
15
+ ```bash
16
+ npm install
17
+ ```
18
+
19
+ 2. **Set your OpenAI API key**:
20
+ Create a `.env` file in this directory or set an environment variable:
21
+
22
+ ```
23
+ OPENAI_API_KEY="your-api-key-here"
24
+ ```
25
+
26
+ 3. **Run the application**:
27
+
28
+ ```bash
29
+ npm start
30
+ ```
31
+
32
+ To specify a topic, pass it as an argument:
33
+
34
+ ```bash
35
+ npm start -- "The Future of Renewable Energy"
36
+ ```
37
+
38
+ ## How It Works
39
+
40
+ The workflow consists of three sequential nodes:
41
+
42
+ ```mermaid
43
+ graph LR
44
+ Outline[Generate Outline] --> Write[Write Content]
45
+ Write --> Style[Apply Style]
@@ -0,0 +1,16 @@
1
+ {
2
+ "name": "basic-workflow",
3
+ "type": "module",
4
+ "scripts": {
5
+ "start": "npx tsx src/main.ts"
6
+ },
7
+ "dependencies": {
8
+ "dotenv": "^16.4.5",
9
+ "flowcraft": "workspace:*",
10
+ "openai": "^4.52.7",
11
+ "yaml": "^2.5.0"
12
+ },
13
+ "devDependencies": {
14
+ "typescript": "^5.5.4"
15
+ }
16
+ }
@@ -0,0 +1,17 @@
1
+ import { Flow } from 'flowcraft'
2
+ import {
3
+ ApplyStyleNode,
4
+ GenerateOutlineNode,
5
+ WriteContentNode,
6
+ } from './nodes'
7
+
8
+ export function createArticleFlow(): Flow {
9
+ const outlineNode = new GenerateOutlineNode()
10
+ const writeContentFlow = new WriteContentNode()
11
+ const styleNode = new ApplyStyleNode()
12
+
13
+ outlineNode.next(writeContentFlow)
14
+ writeContentFlow.next(styleNode)
15
+
16
+ return new Flow(outlineNode)
17
+ }
@@ -0,0 +1,22 @@
1
+ import type { Context } from 'flowcraft'
2
+ import process from 'node:process'
3
+ import dotenv from 'dotenv'
4
+ import { ConsoleLogger, TypedContext } from 'flowcraft'
5
+ import { createArticleFlow } from './flow'
6
+ import { FINAL_ARTICLE, TOPIC } from './nodes'
7
+
8
+ dotenv.config()
9
+
10
+ async function runFlow(topic: string) {
11
+ const context: Context = new TypedContext([[TOPIC, topic]])
12
+ const logger = new ConsoleLogger()
13
+ console.log(`\n=== Starting Article Workflow on Topic: ${topic} ===\n`)
14
+ const flow = createArticleFlow()
15
+ await flow.run(context, { logger })
16
+ console.log('\n=== Workflow Completed ===\n')
17
+ console.log(`Topic: ${context.get(TOPIC)}`)
18
+ console.log(`Final Article Length: ${context.get(FINAL_ARTICLE)?.length || 0} characters`)
19
+ }
20
+
21
+ const topic = process.argv[2] || 'AI Safety'
22
+ runFlow(topic)
@@ -0,0 +1,112 @@
1
+ import type { AbstractNode, NodeArgs } from 'flowcraft'
2
+ import { BatchFlow, contextKey, Node } from 'flowcraft'
3
+ import yaml from 'yaml'
4
+ import { callLLM } from './utils'
5
+
6
+ export const TOPIC = contextKey<string>('topic')
7
+ export const SECTIONS = contextKey<string[]>('sections')
8
+ export const DRAFT = contextKey<string>('draft')
9
+ export const FINAL_ARTICLE = contextKey<string>('final_article')
10
+ export const SECTION_CONTENTS = contextKey<Record<string, string>>('section_contents')
11
+
12
+ /**
13
+ * Generates an article outline from a topic in the context.
14
+ */
15
+ export class GenerateOutlineNode extends Node<string, { sections: string[] }> {
16
+ async prep({ ctx }: NodeArgs): Promise<string> {
17
+ return ctx.get(TOPIC)!
18
+ }
19
+
20
+ async exec({ prepRes: topic }: NodeArgs<string>): Promise<{ sections: string[] }> {
21
+ const prompt = `
22
+ Create a simple outline for an article about "${topic}".
23
+ Include at most 3 main sections (no subsections).
24
+ Output the sections in YAML format as a list under the key "sections".`
25
+ const response = callLLM(prompt)
26
+ const structuredResult = yaml.parse(response)
27
+ return structuredResult
28
+ }
29
+
30
+ async post({ ctx, execRes }: NodeArgs<string, { sections: string[] }>) {
31
+ console.log('\n===== PARSED OUTLINE =====')
32
+ execRes.sections.forEach((s, i) => console.log(`${i + 1}. ${s}`))
33
+ console.log('==========================\n')
34
+ ctx.set(SECTIONS, execRes.sections)
35
+ }
36
+ }
37
+
38
+ /**
39
+ * A BatchFlow that orchestrates writing content for each section of the outline.
40
+ */
41
+ export class WriteContentNode extends BatchFlow {
42
+ protected nodeToRun: AbstractNode = new WriteSingleSectionNode()
43
+
44
+ async prep({ ctx }: NodeArgs) {
45
+ const sections = ctx.get(SECTIONS) || []
46
+ return sections.map(section => ({ section }))
47
+ }
48
+
49
+ async post({ ctx }: NodeArgs) {
50
+ const sectionContents = ctx.get(SECTION_CONTENTS) || {}
51
+ const draft = (ctx.get(SECTIONS) || []).map(section =>
52
+ `## ${section}\n\n${sectionContents[section]}\n`,
53
+ ).join('\n')
54
+ ctx.set(DRAFT, draft)
55
+
56
+ console.log('\n===== SECTION CONTENTS =====\n')
57
+ for (const section in sectionContents)
58
+ console.log(`--- ${section} ---\n${sectionContents[section]}\n`)
59
+
60
+ console.log('============================\n')
61
+ }
62
+ }
63
+
64
+ /**
65
+ * Writes content for a single section, intended to be run by the WriteContentNode batch flow.
66
+ */
67
+ export class WriteSingleSectionNode extends Node<string, string> {
68
+ async prep({ params }: NodeArgs): Promise<string> {
69
+ return params.section
70
+ }
71
+
72
+ async exec({ prepRes: section }: NodeArgs<string>): Promise<string> {
73
+ const prompt = `
74
+ Write a short paragraph (MAXIMUM 100 WORDS) about this section: "${section}".
75
+ Explain the idea in simple, easy-to-understand terms, avoiding jargon.
76
+ Include one brief example or analogy.`
77
+ const content = callLLM(prompt)
78
+ console.log(`āœ“ Completed section: ${section}`)
79
+ return content
80
+ }
81
+
82
+ async post({ ctx, prepRes: section, execRes: content }: NodeArgs<string, string>) {
83
+ const contents = ctx.get(SECTION_CONTENTS) || {}
84
+ contents[section] = content
85
+ ctx.set(SECTION_CONTENTS, contents)
86
+ }
87
+ }
88
+
89
+ /**
90
+ * Applies a final style pass to the complete draft article.
91
+ */
92
+ export class ApplyStyleNode extends Node<string, string> {
93
+ async prep({ ctx }: NodeArgs): Promise<string> {
94
+ return ctx.get(DRAFT) || ''
95
+ }
96
+
97
+ async exec({ prepRes: draft }: NodeArgs<string>): Promise<string> {
98
+ const prompt = `
99
+ Rewrite the following draft in a conversational, engaging style.
100
+ Make it warm in tone, include rhetorical questions, and add a strong opening and conclusion.
101
+
102
+ ${draft}`
103
+ return callLLM(prompt)
104
+ }
105
+
106
+ async post({ ctx, execRes: finalArticle }: NodeArgs<string, string>) {
107
+ ctx.set(FINAL_ARTICLE, finalArticle)
108
+ console.log('\n===== FINAL ARTICLE =====\n')
109
+ console.log(finalArticle)
110
+ console.log('\n=========================\n')
111
+ }
112
+ }
@@ -0,0 +1,35 @@
1
+ export function callLLM(prompt: string): string {
2
+ console.log(`[MOCK LLM] Processing prompt: ${prompt.substring(0, 50)}...`)
3
+
4
+ if (prompt.includes('outline') && prompt.includes('sections')) {
5
+ return `sections:
6
+ - Introduction to the Topic
7
+ - Key Concepts and Applications
8
+ - Future Implications`
9
+ }
10
+
11
+ if (prompt.includes('paragraph') && prompt.includes('100 WORDS')) {
12
+ const section = prompt.match(/"([^"]+)"/)?.[1] || 'the topic'
13
+ return `This section covers ${section} in detail. It's an important concept that helps us understand the broader implications. For example, consider how this applies in real-world scenarios. The key takeaway is that understanding this concept enables better decision-making and practical applications in various contexts.`
14
+ }
15
+
16
+ if (prompt.includes('conversational') && prompt.includes('engaging')) {
17
+ return `Have you ever wondered about this fascinating topic? Let me take you on a journey through these important concepts.
18
+
19
+ ## Introduction to the Topic
20
+
21
+ This section covers Introduction to the Topic in detail. It's an important concept that helps us understand the broader implications. For example, consider how this applies in real-world scenarios. The key takeaway is that understanding this concept enables better decision-making and practical applications in various contexts.
22
+
23
+ ## Key Concepts and Applications
24
+
25
+ This section covers Key Concepts and Applications in detail. It's an important concept that helps us understand the broader implications. For example, consider how this applies in real-world scenarios. The key takeaway is that understanding this concept enables better decision-making and practical applications in various contexts.
26
+
27
+ ## Future Implications
28
+
29
+ This section covers Future Implications in detail. It's an important concept that helps us understand the broader implications. For example, consider how this applies in real-world scenarios. The key takeaway is that understanding this concept enables better decision-making and practical applications in various contexts.
30
+
31
+ In conclusion, these concepts work together to create a comprehensive understanding that will serve you well in future endeavors.`
32
+ }
33
+
34
+ return `Mock response for: ${prompt.substring(0, 100)}`
35
+ }
@@ -0,0 +1,3 @@
1
+ {
2
+ "extends": "../tsconfig.json"
3
+ }
@@ -0,0 +1,46 @@
1
+ # Research Agent
2
+
3
+ This project demonstrates a simple yet powerful research agent that can search the web and answer questions. It uses conditional branching in `workflow` to decide its next action.
4
+
5
+ ## Features
6
+
7
+ - Performs web searches to gather information.
8
+ - Makes decisions about when to search vs. when to answer.
9
+ - Generates comprehensive answers based on research findings.
10
+
11
+ ## How to Run
12
+
13
+ 1. **Install dependencies**:
14
+
15
+ ```bash
16
+ npm install
17
+ ```
18
+
19
+ 2. **Set your OpenAI API key**:
20
+ Create a `.env` file in this directory or set an environment variable:
21
+
22
+ ```
23
+ OPENAI_API_KEY="your-api-key-here"
24
+ ```
25
+
26
+ You will also need a free API key from [SerpApi](https://serpapi.com/) for web search.
27
+
28
+ ```
29
+ SERPAPI_API_KEY="your-serpapi-key-here"
30
+ ```
31
+
32
+ 3. **Run the application**:
33
+
34
+ ```bash
35
+ npm start -- "Who won the Nobel Prize in Physics 2024?"
36
+ ```
37
+
38
+ ## How It Works
39
+
40
+ The agent uses a graph structure with a decision loop:
41
+
42
+ ```mermaid
43
+ graph TD
44
+ A[DecideAction] -->|"search"| B[SearchWeb]
45
+ A -->|"answer"| C[AnswerQuestion]
46
+ B -->|"decide"| A
@@ -0,0 +1,16 @@
1
+ {
2
+ "name": "research-workflow",
3
+ "type": "module",
4
+ "scripts": {
5
+ "start": "npx tsx src/main.ts"
6
+ },
7
+ "dependencies": {
8
+ "dotenv": "^16.4.5",
9
+ "flowcraft": "workspace:*",
10
+ "openai": "^4.52.7",
11
+ "yaml": "^2.5.0"
12
+ },
13
+ "devDependencies": {
14
+ "typescript": "^5.5.4"
15
+ }
16
+ }
@@ -0,0 +1,14 @@
1
+ import { Flow } from 'flowcraft'
2
+ import { AnswerQuestionNode, DecideActionNode, SearchWebNode } from './nodes.js'
3
+
4
+ export function createAgentFlow(): Flow {
5
+ const decideNode = new DecideActionNode()
6
+ const searchNode = new SearchWebNode()
7
+ const answerNode = new AnswerQuestionNode()
8
+
9
+ decideNode.next(searchNode, 'search')
10
+ decideNode.next(answerNode, 'answer')
11
+ searchNode.next(decideNode, 'decide')
12
+
13
+ return new Flow(decideNode)
14
+ }
@@ -0,0 +1,31 @@
1
+ import type { Context } from 'flowcraft'
2
+ import process from 'node:process'
3
+ import dotenv from 'dotenv'
4
+ import { ConsoleLogger, TypedContext } from 'flowcraft'
5
+ import { createAgentFlow } from './flow.js'
6
+
7
+ dotenv.config()
8
+ const MAX_SEARCHES = 2
9
+
10
+ async function runAgent(question: string) {
11
+ if (!question) {
12
+ question = 'Who won the Nobel Prize in Physics 2024?'
13
+ }
14
+
15
+ const context: Context = new TypedContext([
16
+ ['question', question],
17
+ ['search_count', 0],
18
+ ['max_searches', MAX_SEARCHES],
19
+ ])
20
+
21
+ console.log(`šŸ¤” Processing question: ${question}`)
22
+ console.log(`(Agent will stop after ${MAX_SEARCHES} searches)`)
23
+ const agentFlow = createAgentFlow()
24
+ await agentFlow.run(context, { logger: new ConsoleLogger() })
25
+ console.log('\nšŸŽÆ Final Answer:')
26
+
27
+ console.log(context.get<string>('answer') || 'No answer found.')
28
+ }
29
+
30
+ const question = process.argv.slice(2).join(' ')
31
+ runAgent(question)
@@ -0,0 +1,108 @@
1
+ import type { NodeArgs } from 'flowcraft'
2
+ import { DEFAULT_ACTION, Node } from 'flowcraft'
3
+ import yaml from 'yaml'
4
+ import { callLLM, searchWeb } from './utils.js'
5
+
6
+ interface Decision {
7
+ action: 'search' | 'answer'
8
+ reason: string
9
+ search_query?: string
10
+ }
11
+
12
+ interface DecidePrepRes {
13
+ question: string
14
+ context: string
15
+ searchCount: number
16
+ maxSearches: number
17
+ }
18
+
19
+ export class DecideActionNode extends Node<DecidePrepRes, Decision, string> {
20
+ async prep({ ctx }: NodeArgs) {
21
+ const question = ctx.get<string>('question')!
22
+ const context = ctx.get<string>('context') || 'No previous search results.'
23
+ const searchCount = ctx.get<number>('searchCount') || 0
24
+ const maxSearches = ctx.get<number>('maxSearches') ?? 2
25
+ return { question, context, searchCount, maxSearches }
26
+ }
27
+
28
+ async exec({ prepRes }: NodeArgs<DecidePrepRes>) {
29
+ const { question, context, searchCount, maxSearches } = prepRes
30
+ const prompt = `
31
+ You are a research assistant. Based on the question, context, and the number of searches performed, decide whether to search for more information or answer the question.
32
+
33
+ Question: "${question}"
34
+
35
+ Context:
36
+ ${context}
37
+
38
+ Number of searches performed so far: ${searchCount}
39
+
40
+ RULES:
41
+ 1. If the context contains a clear answer to the question, choose 'answer'.
42
+ 2. If the context is insufficient, choose 'search'.
43
+ 3. **ESCAPE HATCH**: If the number of searches is ${maxSearches} or greater, you MUST choose 'answer' to avoid getting stuck in a loop.
44
+
45
+ Return your decision in YAML format inside a markdown code block.
46
+ Your response must include "action" ('search' or 'answer') and a "reason".
47
+ If the action is 'search', you MUST include a "search_query".`
48
+
49
+ const response = await callLLM(prompt)
50
+ const yamlMatch = response.match(/```(?:yaml)?\n([\s\S]*?)\n```/)
51
+ return yaml.parse(yamlMatch ? yamlMatch[1] : response) as Decision
52
+ }
53
+
54
+ async post({ ctx, execRes: decision }: NodeArgs<any, Decision>): Promise<string> {
55
+ console.log(`\nšŸ¤” Agent decides to ${decision?.action}. Reason: ${decision?.reason}`)
56
+ if (decision?.action === 'search') {
57
+ ctx.set('search_query', decision?.search_query)
58
+ console.log(`šŸ” Search Query: ${decision?.search_query}`)
59
+ }
60
+ return decision?.action ?? DEFAULT_ACTION
61
+ }
62
+ }
63
+
64
+ export class SearchWebNode extends Node<string, string, string> {
65
+ async prep({ ctx }: NodeArgs) {
66
+ return ctx.get<string>('search_query')!
67
+ }
68
+
69
+ async exec({ prepRes: query }: NodeArgs<string>) {
70
+ return searchWeb(query)
71
+ }
72
+
73
+ async post({ ctx, prepRes: query, execRes: searchResults }: NodeArgs<string, string>) {
74
+ const currentContext = ctx.get('context') || ''
75
+ const newContext = `${currentContext}\n\nSearch for "${query}":\n${searchResults}`
76
+ ctx.set('context', newContext)
77
+
78
+ const count = ctx.get<number>('searchCount') || 0
79
+ ctx.set('searchCount', count + 1)
80
+
81
+ console.log(`šŸ“š Found information (Search #${count + 1}), analyzing results...`)
82
+ return 'decide'
83
+ }
84
+ }
85
+
86
+ export class AnswerQuestionNode extends Node<{ question: string, context: string }, string> {
87
+ async prep({ ctx }: NodeArgs) {
88
+ const question = ctx.get<string>('question')!
89
+ const context = ctx.get<string>('context') || 'No context provided.'
90
+ return { question, context }
91
+ }
92
+
93
+ async exec({ prepRes }: NodeArgs<{ question: string, context: string }>) {
94
+ const { question, context } = prepRes
95
+ const prompt = `Based on the following context, provide a comprehensive answer to the question.
96
+ Context:
97
+ ${context}
98
+
99
+ Question: "${question}"`
100
+ console.log('āœļø Crafting final answer...')
101
+ return callLLM(prompt)
102
+ }
103
+
104
+ async post({ ctx, execRes: answer }: NodeArgs<any, string>) {
105
+ ctx.set('answer', answer)
106
+ console.log('āœ… Answer generated successfully.')
107
+ }
108
+ }
@@ -0,0 +1,45 @@
1
+ import process from 'node:process'
2
+ import OpenAI from 'openai'
3
+
4
+ const openaiClient = new OpenAI({
5
+ apiKey: process.env.OPENAI_API_KEY,
6
+ })
7
+
8
+ /**
9
+ * Synchronously calls the OpenAI Chat Completions API.
10
+ * @param prompt The user prompt to send to the LLM.
11
+ * @returns The content of the LLM's response as a string.
12
+ */
13
+ export async function callLLM(prompt: string): Promise<string> {
14
+ try {
15
+ const response = await openaiClient.chat.completions.create({
16
+ model: 'gpt-4o-mini',
17
+ messages: [{ role: 'user', content: prompt }],
18
+ })
19
+ return response.choices[0].message.content || ''
20
+ }
21
+ catch (error: any) {
22
+ console.error('Error calling OpenAI API:', error)
23
+ return `Error: Could not get a response from the LLM. Details: ${error.message}`
24
+ }
25
+ }
26
+
27
+ /**
28
+ * Placeholder for a synchronous web search function.
29
+ * In a real application, using an async flow and an async search function is highly recommended.
30
+ * @param query The search query.
31
+ * @returns A formatted string of placeholder search results.
32
+ */
33
+ export function searchWeb(query: string): string {
34
+ console.log(`[SYNC PLACEHOLDER] Searching for: "${query}"`)
35
+ // In a real implementation, you would use a library that makes an async HTTP request.
36
+ return `
37
+ Title: Placeholder Result for ${query}
38
+ URL: http://example.com/search?q=${encodeURIComponent(query)}
39
+ Snippet: This is a placeholder result. For actual web search capabilities, it is highly recommended to use an asynchronous flow (AsyncFlow) with an async-capable search utility.
40
+
41
+ Title: Second Placeholder
42
+ URL: http://example.com/placeholder
43
+ Snippet: Using synchronous I/O in Node.js can block the event loop, impacting performance. Async operations are the standard.
44
+ `.trim()
45
+ }
@@ -0,0 +1,29 @@
1
+ import { DEFAULT_ACTION, Flow, generateMermaidGraph, Node } from 'flowcraft'
2
+
3
+ // Re-create the Research Agent flow from the README
4
+ class DecideActionNode extends Node {
5
+ async post() {
6
+ // Simulate a decision
7
+ const decision = Math.random() > 0.5 ? 'search' : 'answer'
8
+ console.log(`Decision: ${decision}`)
9
+ return decision
10
+ }
11
+ }
12
+
13
+ class SearchWebNode extends Node { }
14
+ class AnswerQuestionNode extends Node { }
15
+
16
+ const decideNode = new DecideActionNode()
17
+ const searchNode = new SearchWebNode()
18
+ const answerNode = new AnswerQuestionNode()
19
+
20
+ // Wire the graph
21
+ decideNode.next(searchNode, 'search')
22
+ decideNode.next(answerNode, 'answer')
23
+ searchNode.next(decideNode, DEFAULT_ACTION) // Loop back
24
+
25
+ const researchAgentFlow = new Flow(decideNode)
26
+
27
+ // Generate and print the Mermaid syntax
28
+ const mermaidGraph = generateMermaidGraph(researchAgentFlow)
29
+ console.log(mermaidGraph)
@@ -0,0 +1,3 @@
1
+ {
2
+ "extends": "../tsconfig.json"
3
+ }