Skip to content

Commit

Permalink
feat: add WorkflowResult (#76)
Browse files Browse the repository at this point in the history
  • Loading branch information
HairlessVillager authored Feb 27, 2025
1 parent 3ce5af6 commit c0cc30b
Show file tree
Hide file tree
Showing 12 changed files with 186 additions and 57 deletions.
8 changes: 5 additions & 3 deletions src/core/eko.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ import {
Tool,
Workflow,
WorkflowCallback,
NodeOutput,
ExecutionContext,
WorkflowResult
} from '../types';
import { ToolRegistry } from './tool-registry';

Expand Down Expand Up @@ -86,7 +86,7 @@ export class Eko {
return workflow;
}

public async execute(workflow: Workflow): Promise<NodeOutput[]> {
public async execute(workflow: Workflow): Promise<WorkflowResult> {
// Inject LLM provider at workflow level
workflow.llmProvider = this.llmProvider;

Expand All @@ -106,7 +106,9 @@ export class Eko {
}
}

return await workflow.execute(this.ekoConfig.callback);
const result = await workflow.execute(this.ekoConfig.callback);
console.log(result);
return result;
}

public async cancel(workflow: Workflow): Promise<void> {
Expand Down
37 changes: 1 addition & 36 deletions src/models/action.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,42 +11,7 @@ import {
LLMResponse,
} from '../types/llm.types';
import { ExecutionLogger } from '@/utils/execution-logger';

/**
* Special tool that allows LLM to write values to context
*/
class WriteContextTool implements Tool<any, any> {
name = 'write_context';
description =
'Write a value to the global workflow context. Use this to store important intermediate results, but only when a piece of information is essential for future reference but missing from the final output specification of the current action.';
input_schema = {
type: 'object',
properties: {
key: {
type: 'string',
description: 'The key to store the value under',
},
value: {
type: 'string',
description: 'The value to store (must be JSON stringified if object/array)',
},
},
required: ['key', 'value'],
} as InputSchema;

async execute(context: ExecutionContext, params: unknown): Promise<unknown> {
const { key, value } = params as { key: string; value: string };
try {
// Try to parse the value as JSON
const parsedValue = JSON.parse(value);
context.variables.set(key, parsedValue);
} catch {
// If parsing fails, store as string
context.variables.set(key, value);
}
return { success: true, key, value };
}
}
import { WriteContextTool } from '@/universal_tools/write_context';

function createReturnTool(
actionName: string,
Expand Down
23 changes: 19 additions & 4 deletions src/models/workflow.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { ExecutionLogger, LogOptions } from "@/utils/execution-logger";
import { Workflow, WorkflowNode, NodeInput, NodeOutput, ExecutionContext, LLMProvider, WorkflowCallback } from "../types";
import { EkoConfig } from "../types/eko.types";
import { Workflow, WorkflowNode, NodeInput, ExecutionContext, LLMProvider, WorkflowCallback } from "../types";
import { EkoConfig, WorkflowResult } from "../types/eko.types";

export class WorkflowImpl implements Workflow {
abort?: boolean;
Expand Down Expand Up @@ -33,7 +33,7 @@ export class WorkflowImpl implements Workflow {
}
}

async execute(callback?: WorkflowCallback): Promise<NodeOutput[]> {
async execute(callback?: WorkflowCallback): Promise<WorkflowResult> {
if (!this.validateDAG()) {
throw new Error("Invalid workflow: Contains circular dependencies");
}
Expand Down Expand Up @@ -122,7 +122,22 @@ export class WorkflowImpl implements Workflow {

callback && await callback.hooks.afterWorkflow?.(this, this.variables);

return terminalNodes.map(node => node.output);
let node_outputs = terminalNodes.map(node => node.output);

// Special context variables
console.log("debug special context variables...");
const workflowIsSuccessful = this.variables.get("workflow_is_successful");
console.log(workflowIsSuccessful);
const workflowSummary = this.variables.get("workflow_summary");
console.log(workflowSummary);
const workflowTranscript = this.variables.get("workflow_transcript");
console.log(workflowTranscript);

return {
isSuccessful: workflowIsSuccessful as boolean,
summary: workflowSummary as string,
payload: workflowTranscript as string,
};
}

addNode(node: WorkflowNode): void {
Expand Down
25 changes: 19 additions & 6 deletions src/services/workflow/generator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,8 +91,26 @@ export class WorkflowGenerator {

const workflowData = response.toolCalls[0].input.workflow as any;

// debug
console.log("Debug the workflow...")
console.log({ ...workflowData});

// Add workflow summary subtask
(workflowData.nodes as any[]).push({
"id": "final",
"type": "action",
"dependencies": workflowData.nodes.map((node: { id: any; }) => node.id),
"action": {
"type": "prompt",
"name": "Summarize the workflow.",
"description": "Summarize briefly what this workflow has accomplished. Your summary should be based on user\'s original query.",
"tools": [
"summary_workflow"
]
},
})
console.log("Debug the workflow...Done")


// Validate all tools exist
for (const node of workflowData.nodes) {
if (!this.toolRegistry.hasTools(node.action.tools)) {
Expand All @@ -105,11 +123,6 @@ export class WorkflowGenerator {
workflowData.id = uuidv4();
}

// debug
console.log("Debug the workflow...")
console.log(workflowData);
console.log("Debug the workflow...Done")

return this.createWorkflowFromData(workflowData, ekoConfig);
}

Expand Down
2 changes: 1 addition & 1 deletion src/services/workflow/templates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ Generate a complete workflow that:
3. Ensures each action has appropriate input/output schemas, and that the "tools" field in each action is populated with the sufficient subset of all available tools needed to complete the action
4. Creates a clear, logical flow to accomplish the user's goal
5. Includes detailed descriptions for each action, ensuring that the actions, when combined, is a complete solution to the user's problem
6. You should always add a SubTask at the end of the workflow to summarize it, and this SubTask should always call the "summary_workflow" tool. It's dependencies should be all of the SubTasks`;
6. Do not use 'summary_workflow' tool`;
},

formatUserPrompt: (requirement: string) =>
Expand Down
10 changes: 10 additions & 0 deletions src/types/eko.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,13 @@ export interface EkoConfig {
export interface EkoInvokeParam {
tools?: Array<string> | Array<Tool<any, any>>;
}

export interface WorkflowResult {
isSuccessful: boolean,
summary: string,
payload: WorkflowTranscript | WorkflowArtifact,
}

export type WorkflowTranscript = string

export interface WorkflowArtifact {} // TODO
15 changes: 15 additions & 0 deletions src/types/tools.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -155,5 +155,20 @@ export interface HumanOperateResult {
}

export interface SummaryWorkflowInput {
isSuccessful: boolean,
summary: string,
}

export interface DocumentAgentToolInput {
type: string,
title: string,
background: string,
keypoints: string,
style?: string,
references?: any,
}

export interface DocumentAgentToolOutput {
status: string,
content: string,
}
3 changes: 2 additions & 1 deletion src/types/workflow.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Action, ExecutionContext, Tool } from "./action.types";
import { LLMProvider } from "./llm.types";
import { ExecutionLogger } from "@/utils/execution-logger";
import { ExportFileParam } from "./tools.types";
import { WorkflowResult } from "./eko.types";

export interface NodeOutput {
name: string;
Expand Down Expand Up @@ -32,7 +33,7 @@ export interface Workflow {
llmProvider?: LLMProvider;

setLogger(logger: ExecutionLogger): void;
execute(callback?: WorkflowCallback): Promise<NodeOutput[]>;
execute(callback?: WorkflowCallback): Promise<WorkflowResult>;
cancel(): Promise<void>;
addNode(node: WorkflowNode): void;
removeNode(nodeId: string): void;
Expand Down
65 changes: 65 additions & 0 deletions src/universal_tools/document_agent.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import {
LLMParameters,
Tool,
InputSchema,
ExecutionContext,
DocumentAgentToolInput,
DocumentAgentToolOutput,
Message,
} from '@/types';

export class DocumentAgentTool implements Tool<DocumentAgentToolInput, DocumentAgentToolOutput> {
name: string;
description: string;
input_schema: InputSchema;

constructor() {
this.name = 'document_agent';
this.description = 'A document agent that can help you write document or long text, e.g. research report, email draft, summary.';
this.input_schema = {
"type": "object",
"properties": {
"type": {
"type": "string",
"description": "The type of document to be created (e.g., 'report', 'presentation', 'article')."
},
"title": {
"type": "string",
"description": "The title of the document."
},
"background": {
"type": "string",
"description": "The background information or target for the document."
},
"keypoints": {
"type": "string",
"description": "A summary of the key points or main ideas to be included in the document."
},
"style": {
"type": "string",
"description": "The desired style or tone of the document (e.g., 'formal', 'casual', 'academic')."
},
},
"required": ["type", "title", "background", "keypoints"],
};
}

async execute(context: ExecutionContext, params: DocumentAgentToolInput): Promise<DocumentAgentToolOutput> {
params.references = context.variables;
const messages: Message[] = [
{
role: 'system',
content: 'You are an excellent writer, skilled at composing various types of copywriting and texts in different styles. You can draft documents based on the title, background, or reference materials provided by clients. Now, the client will provide you with a lot of information, including the type of copywriting, title, background, key points, style, and reference materials. Please write a document in Markdown format.',
},
{
role: 'user',
content: JSON.stringify(params),
},
];
const llmParams: LLMParameters = { maxTokens: 8192 };
const response = await context.llmProvider.generateText(messages, llmParams);
const content = typeof response.content == 'string' ? response.content : (response.content as any[])[0].text;
context.variables.set("workflow_transcript", content);
return { status: "OK", content };
}
}
4 changes: 3 additions & 1 deletion src/universal_tools/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { CancelWorkflow } from "./cancel_workflow";
import { HumanInputText, HumanInputSingleChoice, HumanInputMultipleChoice, HumanOperate } from "./human";
import { SummaryWorkflow } from "./summary_workflow";
import { DocumentAgentTool } from "./document_agent";

export {
CancelWorkflow,
Expand All @@ -9,4 +10,5 @@ export {
HumanInputMultipleChoice,
HumanOperate,
SummaryWorkflow,
}
DocumentAgentTool,
}
16 changes: 11 additions & 5 deletions src/universal_tools/summary_workflow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,17 @@ export class SummaryWorkflow implements Tool<SummaryWorkflowInput, any> {

constructor() {
this.name = 'summary_workflow';
this.description = 'Summarize briefly what this workflow has accomplished.';
this.description = 'Based on the completion of the task assigned by the user, generate the following:\n1. Start by expressing the task status, informing the user whether the task was successfully completed.\n2. Then, briefly and clearly describe the specific outcome of the task.';
this.input_schema = {
type: 'object',
properties: {
isSuccessful: {
type: 'boolean',
description: '`true` if the workflow ultimately executes successfully, and `false` when the workflow ultimately fails, regardless of whether there are errors during the workflow.'
},
summary: {
type: 'string',
description: 'Your summary in markdown format.',
description: 'Your summary in markdown format, include task status and outcome of the task.',
},
},
required: ['summary'],
Expand All @@ -25,9 +29,11 @@ export class SummaryWorkflow implements Tool<SummaryWorkflowInput, any> {
if (typeof params !== 'object' || params === null || !params.summary) {
throw new Error('Invalid parameters. Expected an object with a "summary" property.');
}
const summary = params.summary;
console.log("summary: " + summary);
await context.callback?.hooks.onSummaryWorkflow?.(summary);
console.log("isSuccessful: " + params.isSuccessful);
console.log("summary: " + params.summary);
context.variables.set("workflow_is_successful", params.isSuccessful);
context.variables.set("workflow_summary", params.summary);
await context.callback?.hooks.onSummaryWorkflow?.(params.summary);
return {status: "OK"};
}
}
35 changes: 35 additions & 0 deletions src/universal_tools/write_context.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { Tool, InputSchema, ExecutionContext } from "@/types";

export class WriteContextTool implements Tool<any, any> {
name = 'write_context';
description =
'Write a value to the global workflow context. Use this to store important intermediate results, but only when a piece of information is essential for future reference but missing from the final output specification of the current action.';
input_schema = {
type: 'object',
properties: {
key: {
type: 'string',
description: 'The key to store the value under',
},
value: {
type: 'string',
description: 'The value to store (must be JSON stringified if object/array)',
},
},
required: ['key', 'value'],
} as InputSchema;

async execute(context: ExecutionContext, params: unknown): Promise<unknown> {
const { key, value } = params as { key: string; value: string };
try {
// Try to parse the value as JSON
const parsedValue = JSON.parse(value);
context.variables.set(key, parsedValue);
} catch {
// If parsing fails, store as string
context.variables.set(key, value);
}
return { success: true, key, value };
}
}

0 comments on commit c0cc30b

Please sign in to comment.