Skip to content

Commit 54dfaca

Browse files
committed
feat(flow): add description and toFunction methods for enhanced flow metadata
This commit introduces the `description(name: string, description: string)` method to set flow-level names and descriptions, which are stored in the flow's inferred signature. Additionally, the `toFunction()` method is implemented to convert flows into AxFunction instances, utilizing the flow's inferred signature for generating function metadata. Examples in the documentation and various flow implementations have been updated to demonstrate these new features.
1 parent 5e28406 commit 54dfaca

8 files changed

Lines changed: 160 additions & 19 deletions

File tree

docs/AXFLOW.md

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -305,6 +305,42 @@ flow.r((state) => ({ output: state.value }));
305305
format
306306
- When building reusable flows that need clear output contracts
307307

308+
#### `description(name: string, description: string)`
309+
310+
Set a flow-level name and description. The description is stored on the flow's
311+
inferred signature, and the name/description are used by `toFunction()` for the
312+
exported function metadata.
313+
314+
```typescript
315+
const wf = flow<{ userQuestion: string }, { responseText: string }>()
316+
.node("qa", "userQuestion:string -> responseText:string")
317+
.description(
318+
"Question Answerer",
319+
"Answers user questions concisely using the configured AI model.",
320+
);
321+
```
322+
323+
#### `toFunction()`
324+
325+
Convert the flow into an AxFunction using the flow's inferred signature. The
326+
function's `name` prefers the name set via `description(name, ...)` and falls
327+
back to the first line of the signature description. The `parameters` are
328+
generated from the inferred input fields as JSON Schema.
329+
330+
```typescript
331+
const wf = flow<{ userQuestion: string }, { responseText: string }>()
332+
.node("qa", "userQuestion:string -> responseText:string")
333+
.description(
334+
"Question Answerer",
335+
"Answers user questions concisely using the configured AI model.",
336+
);
337+
338+
const fn = wf.toFunction();
339+
console.log(fn.name); // "questionAnswerer"
340+
console.log(fn.parameters); // JSON Schema from inferred input
341+
// You can call fn.func with args and { ai } to execute the flow
342+
```
343+
308344
### Control Flow Methods
309345

310346
#### `while(condition: Function)` / `endWhile()`

src/ax/flow/flow.ts

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,11 @@ import {
77
SpanKind,
88
trace,
99
} from '@opentelemetry/api';
10-
import type { AxAIService } from '../ai/types.js';
10+
import type {
11+
AxAIService,
12+
AxFunction,
13+
AxFunctionHandler,
14+
} from '../ai/types.js';
1115
import type { AxGen } from '../dsp/generate.js';
1216
import type { AxOptimizedProgram } from '../dsp/optimizer.js';
1317
import { AxProgram } from '../dsp/program.js';
@@ -105,6 +109,9 @@ export class AxFlow<
105109
// Program field that gets initialized when something is added to the graph
106110
private program?: AxProgram<IN, OUT>;
107111

112+
// Optional flow-level name used by toFunction()
113+
private flowName?: string;
114+
108115
// Node-level usage tracking
109116
private nodeUsage: Map<string, AxProgramUsage[]> = new Map();
110117

@@ -713,6 +720,50 @@ export class AxFlow<
713720
this.program!.setDemos(demos);
714721
}
715722

723+
public description(name: string, description: string): this {
724+
this.ensureProgram();
725+
this.flowName = name;
726+
this.program!.setDescription(description);
727+
return this;
728+
}
729+
730+
public toFunction(): AxFunction {
731+
this.ensureProgram();
732+
const sig = this.program!.getSignature();
733+
734+
const baseName =
735+
this.flowName ??
736+
(sig.getDescription()?.trim().split('\n')[0] || 'axFlow');
737+
const safe = baseName.replace(/\s+/g, '_');
738+
const name = this.toCamelCase(safe);
739+
740+
const handler: AxFunctionHandler = async (args?: any, extra?) => {
741+
const ai = extra?.ai;
742+
if (!ai) throw new Error('AI service is required to run the flow');
743+
744+
const ret = await this.forward(ai, (args ?? {}) as IN);
745+
const outFields = sig.getOutputFields();
746+
747+
const resultObj = (ret ?? {}) as Record<string, unknown>;
748+
return Object.keys(resultObj)
749+
.map((k) => {
750+
const f = outFields.find((of) => of.name === k);
751+
if (f && (f as any).title) {
752+
return `${(f as any).title}: ${resultObj[k]}`;
753+
}
754+
return `${k}: ${resultObj[k]}`;
755+
})
756+
.join('\n');
757+
};
758+
759+
return {
760+
name,
761+
description: sig.getDescription() ?? 'Execute this AxFlow',
762+
parameters: sig.toJSONSchema(),
763+
func: handler,
764+
};
765+
}
766+
716767
public getUsage(): AxProgramUsage[] {
717768
// Collect usage from all nodes and merge
718769
const allUsage: AxProgramUsage[] = [];

src/examples/ax-flow-to-function.ts

Lines changed: 12 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -38,26 +38,20 @@ console.log('3. Named flow with function conversion:');
3838
const namedFlow = new AxFlow<
3939
{ userQuestion: string },
4040
{ responseText: string }
41-
>();
41+
>().description(
42+
'Question Answerer',
43+
'Answers user questions concisely using the configured AI model.'
44+
);
4245

4346
// Test function conversion
44-
try {
45-
// toFunction method is not available in the current implementation
46-
const flowAsFunction = {
47-
name: 'Question Answerer',
48-
description: 'Not available',
49-
parameters: { properties: {} },
50-
};
51-
console.log('Function conversion successful:');
52-
console.log('- Name:', flowAsFunction.name);
53-
console.log('- Description:', flowAsFunction.description);
54-
console.log(
55-
'- Parameters schema keys:',
56-
Object.keys(flowAsFunction.parameters?.properties || {})
57-
);
58-
} catch (error) {
59-
console.error('Function conversion failed:', error);
60-
}
47+
const flowAsFunction = namedFlow.toFunction();
48+
console.log('Function conversion successful:');
49+
console.log('- Name:', flowAsFunction.name);
50+
console.log('- Description:', flowAsFunction.description);
51+
console.log(
52+
'- Parameters schema keys:',
53+
Object.keys(flowAsFunction.parameters?.properties || {})
54+
);
6155

6256
// Test direct execution of named flow
6357
const namedResult = await namedFlow.forward(ai, {

src/examples/ax-flow.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@ const llm = ai({
1111
const flowFull = AxFlow.create<{ documentContent: string }>({
1212
autoParallel: true,
1313
})
14+
.description(
15+
'Document Analysis Pipeline',
16+
'Summarizes a document, extracts keywords, and analyzes sentiment.'
17+
)
1418
.node('summarizer', 'documentContent:string -> documentSummary:string')
1519
.node('keywordExtractor', 'documentSummary:string -> keywords:string[]')
1620
.node(
@@ -37,6 +41,10 @@ const flowFull = AxFlow.create<{ documentContent: string }>({
3741

3842
// Aliases example 1 - Customer support ticket processing with error handling
3943
const flowAlias1 = AxFlow.create<{ ticketMessage: string }>()
44+
.description(
45+
'Support Ticket Processor',
46+
'Classifies incoming support messages and generates a suggested response.'
47+
)
4048
.n(
4149
'classifier',
4250
'customerMessage:string -> ticketCategory:string, urgencyLevel:string'
@@ -59,6 +67,10 @@ const flowAlias2 = AxFlow.create<
5967
>({
6068
autoParallel: true,
6169
})
70+
.description(
71+
'Code Review Assistant',
72+
'Analyzes a code snippet and produces a concise review with quality score.'
73+
)
6274
.n(
6375
'codeAnalyzer',
6476
'sourceCode:string -> codeAnalysis:string, qualityScore:number'
@@ -79,6 +91,10 @@ const flowBranch = AxFlow.create<
7991
{ userPost: string; postType: string },
8092
{ moderationAction: string }
8193
>()
94+
.description(
95+
'Content Moderation Router',
96+
'Routes content to the appropriate moderator and returns the moderation action.'
97+
)
8298
.node(
8399
'socialMediaModerator',
84100
'postContent:string -> moderationDecision:string, reasoning:string'
@@ -102,6 +118,10 @@ const flowBranch = AxFlow.create<
102118

103119
// Parallel example - Research paper analysis with manual parallelization
104120
const flowParallel = flow<{ paperAbstract: string }>()
121+
.description(
122+
'Research Paper Scorer',
123+
'Scores novelty and clarity in parallel and computes a combined score.'
124+
)
105125
.node('noveltyScorer', 'researchAbstract:string -> noveltyScore:number')
106126
.node('clarityScorer', 'researchAbstract:string -> clarityScore:number')
107127
.parallel([
@@ -134,6 +154,10 @@ const flowParallel = flow<{ paperAbstract: string }>()
134154

135155
// While example - Iterative writing improvement with circuit breaker
136156
const flowWhile = AxFlow.create<{ draftArticle: string }>()
157+
.description(
158+
'Iterative Writing Improver',
159+
'Loops to improve article quality until the target is reached.'
160+
)
137161
.node(
138162
'qualityEvaluator',
139163
'articleDraft:string -> qualityScore:number, qualityFeedback:string'
@@ -160,6 +184,10 @@ const flowWhile = AxFlow.create<{ draftArticle: string }>()
160184

161185
// Multi-hop RAG example - Research question answering with concurrency control
162186
const flowRAG = AxFlow.create<{ researchQuestion: string }>()
187+
.description(
188+
'Multi-hop Research QA',
189+
'Generates a query, retrieves context, and answers a research question.'
190+
)
163191
.node('queryGenerator', 'researchQuestion:string -> searchQuery:string')
164192
.node('retriever', 'searchQuery:string -> retrievedDocument:string')
165193
.node(
@@ -182,6 +210,10 @@ const flowRAG = AxFlow.create<{ researchQuestion: string }>()
182210

183211
// Batched parallel example - Processing multiple documents with concurrency control
184212
const flowBatchedParallel = flow<{ documentBatch: string }>()
213+
.description(
214+
'Batched Parallel Processor',
215+
'Processes a batch through multiple processors with limited concurrency.'
216+
)
185217
.node('processor1', 'batchData:string -> processedResult1:string')
186218
.node('processor2', 'batchData:string -> processedResult2:string')
187219
.node('processor3', 'batchData:string -> processedResult3:string')

src/examples/flow-type-inference-demo.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@ const _basicFlow = flow<{ userQuestion: string }>().map((state) => ({
99

1010
// Example 2: Complex input type - Full type safety with multiple fields!
1111
const typedFlow = flow<{ userQuestion: string; context: string }>()
12+
.description(
13+
'Type Inference Demo',
14+
'Demonstrates input typing, state evolution, and final typed outputs.'
15+
)
1216
.map((state) => ({
1317
...state,
1418
// TypeScript knows these fields exist!

src/examples/flow-type-safe-output.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@ const llm = ai({ name: 'openai', apiKey: process.env.OPENAI_APIKEY! });
88

99
// Example: Building a type-safe document analysis workflow
1010
const documentAnalyzer = flow<{ documentText: string }>()
11+
.description(
12+
'Type-safe Document Analyzer',
13+
'Summarizes, analyzes sentiment, and extracts keywords with typed output.'
14+
)
1115
// Define reusable nodes
1216
.node('summarizer', 'documentText:string -> summaryText:string')
1317
.node(

src/examples/fluent-flow-example.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@ const documentAnalysisWorkflow = flow<{ userDocument: string }>({
88
debug: true, // Enable logging to see the flow execution
99
autoParallel: true, // Enable automatic parallelization
1010
})
11+
.description(
12+
'Fluent Document Analysis',
13+
'Summarizes a document, analyzes sentiment, and extracts key topics.'
14+
)
1115
// Step 1: Summarize the document
1216
.node(
1317
'summarizer',
@@ -62,6 +66,10 @@ console.log(
6266

6367
// Example of a simpler workflow for comparison
6468
const _simpleWorkflow = flow<{ rawInput: string }>()
69+
.description(
70+
'Simple Processor',
71+
'Processes a rawInput value through a single node and returns the result.'
72+
)
6573
.node('processor', 'userInput:string -> processedOutput:string')
6674
.execute('processor', (state) => ({ userInput: state.rawInput || 'default' }))
6775
.map((state) => ({
@@ -72,6 +80,10 @@ console.log('\nSimple workflow also created successfully!');
7280

7381
// Example showing different node creation methods
7482
const _advancedWorkflow = flow<{ inputDocument: string }>()
83+
.description(
84+
'Advanced Text Analyzer',
85+
'Analyzes text and returns a final analysis using multiple steps.'
86+
)
7587
// Basic string signature
7688
.node('textAnalyzer', 'documentText:string -> analysisResult:string')
7789

@@ -97,6 +109,10 @@ console.log('✅ Automatic parallelization and optimization');
97109

98110
// Show how to extend workflows
99111
const _extendedWorkflow = flow<{ sourceData: string }>()
112+
.description(
113+
'Extended Workflow',
114+
'Demonstrates chaining, execution, and mapping across multiple steps.'
115+
)
100116
.node('step1', 'userInput:string -> intermediateResult:string')
101117
.execute('step1', (state) => ({ userInput: state.sourceData || 'test' }))
102118

src/examples/gepa-flow.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@ import { AxAI, AxAIOpenAIModel, AxGEPAFlow, flow } from '@ax-llm/ax';
22

33
// Two-objective flow: classify priority and produce a brief rationale
44
const flowEmail = flow<{ emailText: string }>()
5+
.description(
6+
'Email Priority Classifier',
7+
'Classifies an email priority and produces a concise rationale.'
8+
)
59
.n('classifier', 'emailText:string -> priority:class "high, normal, low"')
610
.n(
711
'rationale',

0 commit comments

Comments
 (0)