redesign fully scaffolded and web login works
This commit is contained in:
461
gateway/src/harness/workflows/README.md
Normal file
461
gateway/src/harness/workflows/README.md
Normal file
@@ -0,0 +1,461 @@
|
||||
# Workflows
|
||||
|
||||
LangGraph-based workflows for multi-step agent orchestration.
|
||||
|
||||
## What are Workflows?
|
||||
|
||||
Workflows are state machines that orchestrate complex multi-step tasks with:
|
||||
|
||||
- **State Management**: Typed state with annotations
|
||||
- **Conditional Routing**: Different paths based on state
|
||||
- **Validation Loops**: Retry with fixes
|
||||
- **Human-in-the-Loop**: Approval gates and interrupts
|
||||
- **Error Recovery**: Graceful handling of failures
|
||||
|
||||
Built on [LangGraph.js](https://langchain-ai.github.io/langgraphjs/).
|
||||
|
||||
## Directory Structure
|
||||
|
||||
```
|
||||
workflows/
|
||||
├── base-workflow.ts # Base class and utilities
|
||||
├── {workflow-name}/
|
||||
│ ├── config.yaml # Workflow configuration
|
||||
│ ├── state.ts # State schema (Annotations)
|
||||
│ ├── nodes.ts # Node implementations
|
||||
│ └── graph.ts # StateGraph definition
|
||||
└── README.md # This file
|
||||
```
|
||||
|
||||
## Workflow Components
|
||||
|
||||
### State (state.ts)
|
||||
|
||||
Defines what data flows through the workflow:
|
||||
|
||||
```typescript
|
||||
import { Annotation } from '@langchain/langgraph';
|
||||
import { BaseWorkflowState } from '../base-workflow.js';
|
||||
|
||||
export const MyWorkflowState = Annotation.Root({
|
||||
...BaseWorkflowState.spec, // Inherit base fields
|
||||
|
||||
// Your custom fields
|
||||
input: Annotation<string>(),
|
||||
result: Annotation<string | null>({ default: () => null }),
|
||||
errorCount: Annotation<number>({ default: () => 0 }),
|
||||
});
|
||||
|
||||
export type MyWorkflowStateType = typeof MyWorkflowState.State;
|
||||
```
|
||||
|
||||
### Nodes (nodes.ts)
|
||||
|
||||
Functions that transform state:
|
||||
|
||||
```typescript
|
||||
export function createMyNode(deps: Dependencies) {
|
||||
return async (state: MyWorkflowStateType): Promise<Partial<MyWorkflowStateType>> => {
|
||||
// Do work
|
||||
const result = await doSomething(state.input);
|
||||
|
||||
// Return partial state update
|
||||
return { result };
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
### Graph (graph.ts)
|
||||
|
||||
Connects nodes with edges:
|
||||
|
||||
```typescript
|
||||
import { StateGraph } from '@langchain/langgraph';
|
||||
import { BaseWorkflow } from '../base-workflow.js';
|
||||
|
||||
export class MyWorkflow extends BaseWorkflow<MyWorkflowStateType> {
|
||||
buildGraph(): StateGraph<MyWorkflowStateType> {
|
||||
const graph = new StateGraph(MyWorkflowState);
|
||||
|
||||
// Add nodes
|
||||
graph
|
||||
.addNode('step1', createStep1Node())
|
||||
.addNode('step2', createStep2Node());
|
||||
|
||||
// Add edges
|
||||
graph
|
||||
.addEdge('__start__', 'step1')
|
||||
.addEdge('step1', 'step2')
|
||||
.addEdge('step2', '__end__');
|
||||
|
||||
return graph;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Config (config.yaml)
|
||||
|
||||
Workflow settings:
|
||||
|
||||
```yaml
|
||||
name: my-workflow
|
||||
description: What it does
|
||||
|
||||
timeout: 300000 # 5 minutes
|
||||
maxRetries: 3
|
||||
requiresApproval: true
|
||||
approvalNodes:
|
||||
- human_approval
|
||||
|
||||
# Custom settings
|
||||
myCustomSetting: value
|
||||
```
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### 1. Validation Loop (Retry with Fixes)
|
||||
|
||||
```typescript
|
||||
graph
|
||||
.addNode('validate', validateNode)
|
||||
.addNode('fix', fixNode)
|
||||
.addConditionalEdges('validate', (state) => {
|
||||
if (state.isValid) return 'next_step';
|
||||
if (state.retryCount >= 3) return '__end__'; // Give up
|
||||
return 'fix'; // Try to fix
|
||||
})
|
||||
.addEdge('fix', 'validate'); // Loop back
|
||||
```
|
||||
|
||||
### 2. Human-in-the-Loop (Approval)
|
||||
|
||||
```typescript
|
||||
const approvalNode = async (state) => {
|
||||
// Send approval request to user's channel
|
||||
await sendToChannel(state.userContext.activeChannel, {
|
||||
type: 'approval_request',
|
||||
data: {
|
||||
action: 'execute_trade',
|
||||
details: state.tradeDetails,
|
||||
}
|
||||
});
|
||||
|
||||
// Mark as waiting for approval
|
||||
return { approvalRequested: true, userApproved: false };
|
||||
};
|
||||
|
||||
graph.addConditionalEdges('approval', (state) => {
|
||||
return state.userApproved ? 'execute' : '__end__';
|
||||
});
|
||||
|
||||
// To resume after user input:
|
||||
// const updated = await workflow.execute({ ...state, userApproved: true });
|
||||
```
|
||||
|
||||
### 3. Parallel Execution
|
||||
|
||||
```typescript
|
||||
import { Branch } from '@langchain/langgraph';
|
||||
|
||||
graph
|
||||
.addNode('parallel_start', startNode)
|
||||
.addNode('task_a', taskANode)
|
||||
.addNode('task_b', taskBNode)
|
||||
.addNode('merge', mergeNode);
|
||||
|
||||
// Branch to parallel tasks
|
||||
graph.addEdge('parallel_start', Branch.parallel(['task_a', 'task_b']));
|
||||
|
||||
// Merge results
|
||||
graph
|
||||
.addEdge('task_a', 'merge')
|
||||
.addEdge('task_b', 'merge');
|
||||
```
|
||||
|
||||
### 4. Error Recovery
|
||||
|
||||
```typescript
|
||||
const resilientNode = async (state) => {
|
||||
try {
|
||||
const result = await riskyOperation();
|
||||
return { result, error: null };
|
||||
} catch (error) {
|
||||
logger.error({ error }, 'Operation failed');
|
||||
return {
|
||||
error: error.message,
|
||||
fallbackUsed: true,
|
||||
result: await fallbackOperation()
|
||||
};
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
### 5. Conditional Routing
|
||||
|
||||
```typescript
|
||||
graph.addConditionalEdges('decision', (state) => {
|
||||
if (state.score > 0.8) return 'high_confidence';
|
||||
if (state.score > 0.5) return 'medium_confidence';
|
||||
return 'low_confidence';
|
||||
});
|
||||
|
||||
graph
|
||||
.addNode('high_confidence', autoApproveNode)
|
||||
.addNode('medium_confidence', humanReviewNode)
|
||||
.addNode('low_confidence', rejectNode);
|
||||
```
|
||||
|
||||
## Available Workflows
|
||||
|
||||
### strategy-validation
|
||||
|
||||
Validates trading strategies with multiple steps and a validation loop.
|
||||
|
||||
**Flow:**
|
||||
1. Code Review (using CodeReviewerSubagent)
|
||||
2. If issues → Fix Code → loop back
|
||||
3. Backtest (via MCP)
|
||||
4. If failed → Fix Code → loop back
|
||||
5. Risk Assessment
|
||||
6. Human Approval
|
||||
7. Final Recommendation
|
||||
|
||||
**Features:**
|
||||
- Max 3 retry attempts
|
||||
- Multi-file memory from subagent
|
||||
- Risk-based auto-approval
|
||||
- Comprehensive state tracking
|
||||
|
||||
### trading-request
|
||||
|
||||
Human-in-the-loop workflow for trade execution.
|
||||
|
||||
**Flow:**
|
||||
1. Analyze market conditions
|
||||
2. Calculate risk and position size
|
||||
3. Request human approval (PAUSE)
|
||||
4. If approved → Execute trade
|
||||
5. Generate summary
|
||||
|
||||
**Features:**
|
||||
- Interrupt at approval node
|
||||
- Channel-aware approval UI
|
||||
- Risk validation
|
||||
- Execution confirmation
|
||||
|
||||
## Creating a New Workflow
|
||||
|
||||
### 1. Create Directory
|
||||
|
||||
```bash
|
||||
mkdir -p workflows/my-workflow
|
||||
```
|
||||
|
||||
### 2. Define State
|
||||
|
||||
```typescript
|
||||
// state.ts
|
||||
import { Annotation } from '@langchain/langgraph';
|
||||
import { BaseWorkflowState } from '../base-workflow.js';
|
||||
|
||||
export const MyWorkflowState = Annotation.Root({
|
||||
...BaseWorkflowState.spec,
|
||||
|
||||
// Add your fields
|
||||
input: Annotation<string>(),
|
||||
step1Result: Annotation<string | null>({ default: () => null }),
|
||||
step2Result: Annotation<string | null>({ default: () => null }),
|
||||
});
|
||||
|
||||
export type MyWorkflowStateType = typeof MyWorkflowState.State;
|
||||
```
|
||||
|
||||
### 3. Create Nodes
|
||||
|
||||
```typescript
|
||||
// nodes.ts
|
||||
import { MyWorkflowStateType } from './state.js';
|
||||
|
||||
export function createStep1Node(deps: any) {
|
||||
return async (state: MyWorkflowStateType) => {
|
||||
const result = await doStep1(state.input);
|
||||
return { step1Result: result };
|
||||
};
|
||||
}
|
||||
|
||||
export function createStep2Node(deps: any) {
|
||||
return async (state: MyWorkflowStateType) => {
|
||||
const result = await doStep2(state.step1Result);
|
||||
return { step2Result: result, output: result };
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Build Graph
|
||||
|
||||
```typescript
|
||||
// graph.ts
|
||||
import { StateGraph } from '@langchain/langgraph';
|
||||
import { BaseWorkflow, WorkflowConfig } from '../base-workflow.js';
|
||||
import { MyWorkflowState, MyWorkflowStateType } from './state.js';
|
||||
import { createStep1Node, createStep2Node } from './nodes.js';
|
||||
|
||||
export class MyWorkflow extends BaseWorkflow<MyWorkflowStateType> {
|
||||
constructor(config: WorkflowConfig, private deps: any, logger: Logger) {
|
||||
super(config, logger);
|
||||
}
|
||||
|
||||
buildGraph(): StateGraph<MyWorkflowStateType> {
|
||||
const graph = new StateGraph(MyWorkflowState);
|
||||
|
||||
const step1 = createStep1Node(this.deps);
|
||||
const step2 = createStep2Node(this.deps);
|
||||
|
||||
graph
|
||||
.addNode('step1', step1)
|
||||
.addNode('step2', step2)
|
||||
.addEdge('__start__', 'step1')
|
||||
.addEdge('step1', 'step2')
|
||||
.addEdge('step2', '__end__');
|
||||
|
||||
return graph;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 5. Create Config
|
||||
|
||||
```yaml
|
||||
# config.yaml
|
||||
name: my-workflow
|
||||
description: My workflow description
|
||||
|
||||
timeout: 60000
|
||||
maxRetries: 3
|
||||
requiresApproval: false
|
||||
|
||||
model: claude-3-5-sonnet-20241022
|
||||
```
|
||||
|
||||
### 6. Add Factory Function
|
||||
|
||||
```typescript
|
||||
// graph.ts (continued)
|
||||
export async function createMyWorkflow(
|
||||
deps: any,
|
||||
logger: Logger,
|
||||
configPath: string
|
||||
): Promise<MyWorkflow> {
|
||||
const config = await loadYAML(configPath);
|
||||
const workflow = new MyWorkflow(config, deps, logger);
|
||||
workflow.compile();
|
||||
return workflow;
|
||||
}
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### Execute Workflow
|
||||
|
||||
```typescript
|
||||
import { createMyWorkflow } from './harness/workflows';
|
||||
|
||||
const workflow = await createMyWorkflow(deps, logger, configPath);
|
||||
|
||||
const result = await workflow.execute({
|
||||
userContext,
|
||||
input: 'my input'
|
||||
});
|
||||
|
||||
console.log(result.output);
|
||||
```
|
||||
|
||||
### Stream Workflow
|
||||
|
||||
```typescript
|
||||
for await (const state of workflow.stream({ userContext, input })) {
|
||||
console.log('Current state:', state);
|
||||
}
|
||||
```
|
||||
|
||||
### With Interrupts (Human-in-the-Loop)
|
||||
|
||||
```typescript
|
||||
// Initial execution (pauses at interrupt)
|
||||
const pausedState = await workflow.execute(initialState);
|
||||
|
||||
// User provides input
|
||||
const userInput = await getUserApproval();
|
||||
|
||||
// Resume from paused state
|
||||
const finalState = await workflow.execute({
|
||||
...pausedState,
|
||||
userApproved: userInput.approved
|
||||
});
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### State Design
|
||||
|
||||
- **Immutable Updates**: Return partial state, don't mutate
|
||||
- **Type Safety**: Use TypeScript annotations
|
||||
- **Defaults**: Provide sensible defaults
|
||||
- **Nullable Fields**: Use `| null` with `default: () => null`
|
||||
|
||||
### Node Implementation
|
||||
|
||||
- **Pure Functions**: Avoid side effects in state logic
|
||||
- **Error Handling**: Catch errors, return error state
|
||||
- **Logging**: Log entry/exit of nodes
|
||||
- **Partial Updates**: Only return fields that changed
|
||||
|
||||
### Graph Design
|
||||
|
||||
- **Single Responsibility**: Each node does one thing
|
||||
- **Clear Flow**: Easy to visualize the graph
|
||||
- **Error Paths**: Handle failures gracefully
|
||||
- **Idempotency**: Safe to retry nodes
|
||||
|
||||
### Configuration
|
||||
|
||||
- **Timeouts**: Set reasonable limits
|
||||
- **Retries**: Don't retry forever
|
||||
- **Approvals**: Mark approval nodes explicitly
|
||||
- **Documentation**: Explain complex config values
|
||||
|
||||
## Debugging
|
||||
|
||||
### View Graph
|
||||
|
||||
```typescript
|
||||
// Get graph structure
|
||||
const compiled = workflow.compile();
|
||||
console.log(compiled.getGraph());
|
||||
```
|
||||
|
||||
### Log State
|
||||
|
||||
```typescript
|
||||
const debugNode = async (state) => {
|
||||
logger.debug({ state }, 'Current state');
|
||||
return {}; // No changes
|
||||
};
|
||||
|
||||
graph.addNode('debug', debugNode);
|
||||
```
|
||||
|
||||
### Test Nodes in Isolation
|
||||
|
||||
```typescript
|
||||
const step1 = createStep1Node(deps);
|
||||
const result = await step1({ input: 'test', /* ... */ });
|
||||
expect(result.step1Result).toBe('expected');
|
||||
```
|
||||
|
||||
## References
|
||||
|
||||
- [LangGraph.js Docs](https://langchain-ai.github.io/langgraphjs/)
|
||||
- [LangChain.js Docs](https://js.langchain.com/)
|
||||
- [Example: strategy-validation](./strategy-validation/graph.ts)
|
||||
- [Example: trading-request](./trading-request/graph.ts)
|
||||
200
gateway/src/harness/workflows/base-workflow.ts
Normal file
200
gateway/src/harness/workflows/base-workflow.ts
Normal file
@@ -0,0 +1,200 @@
|
||||
import { Annotation } from '@langchain/langgraph';
|
||||
import type { FastifyBaseLogger } from 'fastify';
|
||||
import type { UserContext } from '../memory/session-context.js';
|
||||
|
||||
/**
|
||||
* Workflow configuration (loaded from config.yaml)
|
||||
*/
|
||||
export interface WorkflowConfig {
|
||||
name: string;
|
||||
description: string;
|
||||
timeout?: number; // Milliseconds
|
||||
maxRetries?: number;
|
||||
requiresApproval?: boolean;
|
||||
approvalNodes?: string[]; // Nodes that require human approval
|
||||
}
|
||||
|
||||
/**
|
||||
* Base workflow state (all workflows extend this)
|
||||
*/
|
||||
export const BaseWorkflowState = Annotation.Root({
|
||||
userContext: Annotation<UserContext>(),
|
||||
input: Annotation<string>(),
|
||||
output: Annotation<string | null>({
|
||||
value: (left, right) => right ?? left,
|
||||
default: () => null,
|
||||
}),
|
||||
error: Annotation<string | null>({
|
||||
value: (left, right) => right ?? left,
|
||||
default: () => null,
|
||||
}),
|
||||
metadata: Annotation<Record<string, unknown>>({
|
||||
value: (left, right) => ({ ...left, ...right }),
|
||||
default: () => ({}),
|
||||
}),
|
||||
});
|
||||
|
||||
export type BaseWorkflowStateType = typeof BaseWorkflowState.State;
|
||||
|
||||
/**
|
||||
* Workflow node function type
|
||||
*/
|
||||
export type WorkflowNode<TState> = (state: TState) => Promise<Partial<TState>>;
|
||||
|
||||
/**
|
||||
* Workflow edge condition function type
|
||||
*/
|
||||
export type WorkflowEdgeCondition<TState> = (state: TState) => string;
|
||||
|
||||
/**
|
||||
* Base workflow class
|
||||
*
|
||||
* Workflows are LangGraph state machines with:
|
||||
* - Config-driven setup (timeout, retries, approval gates)
|
||||
* - Standardized state structure
|
||||
* - Support for human-in-the-loop
|
||||
* - Validation loops
|
||||
* - Error handling
|
||||
*
|
||||
* Structure:
|
||||
* workflows/
|
||||
* strategy-validation/
|
||||
* config.yaml
|
||||
* graph.ts
|
||||
* nodes.ts
|
||||
* state.ts
|
||||
*/
|
||||
export abstract class BaseWorkflow<TState extends BaseWorkflowStateType> {
|
||||
protected logger: FastifyBaseLogger;
|
||||
protected config: WorkflowConfig;
|
||||
protected graph?: any;
|
||||
|
||||
constructor(config: WorkflowConfig, logger: FastifyBaseLogger) {
|
||||
this.config = config;
|
||||
this.logger = logger;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the workflow graph (implemented by subclasses)
|
||||
*/
|
||||
abstract buildGraph(): any;
|
||||
|
||||
/**
|
||||
* Compile the workflow graph
|
||||
*/
|
||||
compile(): void {
|
||||
this.logger.info({ workflow: this.config.name }, 'Compiling workflow graph');
|
||||
const stateGraph = this.buildGraph();
|
||||
this.graph = stateGraph.compile();
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the workflow
|
||||
*/
|
||||
async execute(initialState: Partial<TState>): Promise<TState> {
|
||||
if (!this.graph) {
|
||||
throw new Error('Workflow not compiled. Call compile() first.');
|
||||
}
|
||||
|
||||
this.logger.info(
|
||||
{ workflow: this.config.name, userId: initialState.userContext?.userId },
|
||||
'Executing workflow'
|
||||
);
|
||||
|
||||
const startTime = Date.now();
|
||||
|
||||
try {
|
||||
// Execute with timeout if configured
|
||||
const result = this.config.timeout
|
||||
? await this.executeWithTimeout(initialState)
|
||||
: await this.graph.invoke(initialState);
|
||||
|
||||
const duration = Date.now() - startTime;
|
||||
|
||||
this.logger.info(
|
||||
{
|
||||
workflow: this.config.name,
|
||||
duration,
|
||||
success: !result.error,
|
||||
},
|
||||
'Workflow execution completed'
|
||||
);
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
this.logger.error(
|
||||
{ error, workflow: this.config.name },
|
||||
'Workflow execution failed'
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stream workflow execution
|
||||
*/
|
||||
async *stream(initialState: Partial<TState>): AsyncGenerator<TState> {
|
||||
if (!this.graph) {
|
||||
throw new Error('Workflow not compiled. Call compile() first.');
|
||||
}
|
||||
|
||||
this.logger.info(
|
||||
{ workflow: this.config.name, userId: initialState.userContext?.userId },
|
||||
'Streaming workflow execution'
|
||||
);
|
||||
|
||||
try {
|
||||
const stream = await this.graph.stream(initialState);
|
||||
|
||||
for await (const state of stream) {
|
||||
yield state;
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.error(
|
||||
{ error, workflow: this.config.name },
|
||||
'Workflow streaming failed'
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute with timeout
|
||||
*/
|
||||
private async executeWithTimeout(initialState: Partial<TState>): Promise<TState> {
|
||||
if (!this.config.timeout || !this.graph) {
|
||||
throw new Error('Invalid state');
|
||||
}
|
||||
|
||||
return await Promise.race([
|
||||
this.graph.invoke(initialState) as Promise<TState>,
|
||||
new Promise<TState>((_, reject) =>
|
||||
setTimeout(
|
||||
() => reject(new Error(`Workflow timeout after ${this.config.timeout}ms`)),
|
||||
this.config.timeout
|
||||
)
|
||||
),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get workflow name
|
||||
*/
|
||||
getName(): string {
|
||||
return this.config.name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if workflow requires approval
|
||||
*/
|
||||
requiresApproval(): boolean {
|
||||
return this.config.requiresApproval || false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get approval nodes
|
||||
*/
|
||||
getApprovalNodes(): string[] {
|
||||
return this.config.approvalNodes || [];
|
||||
}
|
||||
}
|
||||
20
gateway/src/harness/workflows/index.ts
Normal file
20
gateway/src/harness/workflows/index.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
// Workflows exports
|
||||
|
||||
export {
|
||||
BaseWorkflow,
|
||||
BaseWorkflowState,
|
||||
type WorkflowConfig,
|
||||
type BaseWorkflowStateType,
|
||||
type WorkflowNode,
|
||||
type WorkflowEdgeCondition,
|
||||
} from './base-workflow.js';
|
||||
|
||||
export {
|
||||
StrategyValidationWorkflow,
|
||||
createStrategyValidationWorkflow,
|
||||
} from './strategy-validation/graph.js';
|
||||
|
||||
export {
|
||||
TradingRequestWorkflow,
|
||||
createTradingRequestWorkflow,
|
||||
} from './trading-request/graph.js';
|
||||
@@ -0,0 +1,19 @@
|
||||
# Strategy Validation Workflow Configuration
|
||||
|
||||
name: strategy-validation
|
||||
description: Validates trading strategies with code review, backtest, and risk assessment
|
||||
|
||||
# Workflow settings
|
||||
timeout: 300000 # 5 minutes
|
||||
maxRetries: 3
|
||||
requiresApproval: true
|
||||
approvalNodes:
|
||||
- human_approval
|
||||
|
||||
# Validation loop settings
|
||||
maxValidationRetries: 3 # Max times to retry fixing errors
|
||||
minBacktestScore: 0.5 # Minimum Sharpe ratio to pass
|
||||
|
||||
# Model override (optional)
|
||||
model: claude-3-5-sonnet-20241022
|
||||
temperature: 0.3
|
||||
138
gateway/src/harness/workflows/strategy-validation/graph.ts
Normal file
138
gateway/src/harness/workflows/strategy-validation/graph.ts
Normal file
@@ -0,0 +1,138 @@
|
||||
import { StateGraph } from '@langchain/langgraph';
|
||||
import { BaseWorkflow, type WorkflowConfig } from '../base-workflow.js';
|
||||
import { StrategyValidationState, type StrategyValidationStateType } from './state.js';
|
||||
import {
|
||||
createCodeReviewNode,
|
||||
createFixCodeNode,
|
||||
createBacktestNode,
|
||||
createRiskAssessmentNode,
|
||||
createHumanApprovalNode,
|
||||
createRecommendationNode,
|
||||
} from './nodes.js';
|
||||
import type { FastifyBaseLogger } from 'fastify';
|
||||
import type { BaseChatModel } from '@langchain/core/language_models/chat_models';
|
||||
import type { CodeReviewerSubagent } from '../../subagents/code-reviewer/index.js';
|
||||
|
||||
/**
|
||||
* Strategy Validation Workflow
|
||||
*
|
||||
* Multi-step workflow with validation loop:
|
||||
* 1. Code Review (using CodeReviewerSubagent)
|
||||
* 2. If issues found → Fix Code → Loop back to Code Review
|
||||
* 3. Backtest (using user's MCP server)
|
||||
* 4. If backtest fails → Fix Code → Loop back to Code Review
|
||||
* 5. Risk Assessment
|
||||
* 6. Human Approval (pause for user input)
|
||||
* 7. Final Recommendation
|
||||
*
|
||||
* Features:
|
||||
* - Validation loop with max retries
|
||||
* - Human-in-the-loop approval gate
|
||||
* - Multi-file memory from CodeReviewerSubagent
|
||||
* - Comprehensive state tracking
|
||||
*/
|
||||
export class StrategyValidationWorkflow extends BaseWorkflow<StrategyValidationStateType> {
|
||||
constructor(
|
||||
config: WorkflowConfig,
|
||||
private model: BaseChatModel,
|
||||
private codeReviewer: CodeReviewerSubagent,
|
||||
private mcpBacktestFn: (code: string, ticker: string, timeframe: string) => Promise<Record<string, unknown>>,
|
||||
logger: FastifyBaseLogger
|
||||
) {
|
||||
super(config, logger);
|
||||
}
|
||||
|
||||
buildGraph(): any {
|
||||
const graph = new StateGraph(StrategyValidationState);
|
||||
|
||||
// Create nodes
|
||||
const codeReviewNode = createCodeReviewNode(this.codeReviewer, this.logger);
|
||||
const fixCodeNode = createFixCodeNode(this.model, this.logger);
|
||||
const backtestNode = createBacktestNode(this.mcpBacktestFn, this.logger);
|
||||
const riskAssessmentNode = createRiskAssessmentNode(this.model, this.logger);
|
||||
const humanApprovalNode = createHumanApprovalNode(this.logger);
|
||||
const recommendationNode = createRecommendationNode(this.model, this.logger);
|
||||
|
||||
// Add nodes to graph
|
||||
graph
|
||||
.addNode('code_review', codeReviewNode)
|
||||
.addNode('fix_code', fixCodeNode)
|
||||
.addNode('backtest', backtestNode)
|
||||
.addNode('risk_assessment', riskAssessmentNode)
|
||||
.addNode('human_approval', humanApprovalNode)
|
||||
.addNode('recommendation', recommendationNode);
|
||||
|
||||
// Define edges
|
||||
(graph as any).addEdge('__start__', 'code_review');
|
||||
|
||||
// Conditional: After code review, fix if needed or proceed to backtest
|
||||
(graph as any).addConditionalEdges('code_review', (state: any) => {
|
||||
if (state.needsFixing && state.validationRetryCount < 3) {
|
||||
return 'fix_code';
|
||||
}
|
||||
if (state.needsFixing && state.validationRetryCount >= 3) {
|
||||
return 'recommendation'; // Give up, generate rejection
|
||||
}
|
||||
return 'backtest';
|
||||
});
|
||||
|
||||
// After fixing code, loop back to code review
|
||||
(graph as any).addEdge('fix_code', 'code_review');
|
||||
|
||||
// Conditional: After backtest, fix if failed or proceed to risk assessment
|
||||
(graph as any).addConditionalEdges('backtest', (state: any) => {
|
||||
if (!state.backtestPassed && state.validationRetryCount < 3) {
|
||||
return 'fix_code';
|
||||
}
|
||||
if (!state.backtestPassed && state.validationRetryCount >= 3) {
|
||||
return 'recommendation'; // Give up
|
||||
}
|
||||
return 'risk_assessment';
|
||||
});
|
||||
|
||||
// After risk assessment, go to human approval
|
||||
(graph as any).addEdge('risk_assessment', 'human_approval');
|
||||
|
||||
// Conditional: After human approval, proceed to recommendation or reject
|
||||
(graph as any).addConditionalEdges('human_approval', (state: any) => {
|
||||
return state.humanApproved ? 'recommendation' : '__end__';
|
||||
});
|
||||
|
||||
// Final recommendation is terminal
|
||||
(graph as any).addEdge('recommendation', '__end__');
|
||||
|
||||
return graph;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory function to create and compile workflow
|
||||
*/
|
||||
export async function createStrategyValidationWorkflow(
|
||||
model: BaseChatModel,
|
||||
codeReviewer: CodeReviewerSubagent,
|
||||
mcpBacktestFn: (code: string, ticker: string, timeframe: string) => Promise<Record<string, unknown>>,
|
||||
logger: FastifyBaseLogger,
|
||||
configPath: string
|
||||
): Promise<StrategyValidationWorkflow> {
|
||||
const { readFile } = await import('fs/promises');
|
||||
const yaml = await import('js-yaml');
|
||||
|
||||
// Load config
|
||||
const configContent = await readFile(configPath, 'utf-8');
|
||||
const config = yaml.load(configContent) as WorkflowConfig;
|
||||
|
||||
// Create workflow
|
||||
const workflow = new StrategyValidationWorkflow(
|
||||
config,
|
||||
model,
|
||||
codeReviewer,
|
||||
mcpBacktestFn,
|
||||
logger
|
||||
);
|
||||
|
||||
// Compile graph
|
||||
workflow.compile();
|
||||
|
||||
return workflow;
|
||||
}
|
||||
233
gateway/src/harness/workflows/strategy-validation/nodes.ts
Normal file
233
gateway/src/harness/workflows/strategy-validation/nodes.ts
Normal file
@@ -0,0 +1,233 @@
|
||||
import type { StrategyValidationStateType } from './state.js';
|
||||
import type { FastifyBaseLogger } from 'fastify';
|
||||
import type { BaseChatModel } from '@langchain/core/language_models/chat_models';
|
||||
import type { CodeReviewerSubagent } from '../../subagents/code-reviewer/index.js';
|
||||
import { HumanMessage, SystemMessage } from '@langchain/core/messages';
|
||||
|
||||
/**
|
||||
* Node: Code Review
|
||||
* Reviews strategy code using CodeReviewerSubagent
|
||||
*/
|
||||
export function createCodeReviewNode(
|
||||
codeReviewer: CodeReviewerSubagent,
|
||||
logger: FastifyBaseLogger
|
||||
) {
|
||||
return async (state: StrategyValidationStateType): Promise<Partial<StrategyValidationStateType>> => {
|
||||
logger.info('Strategy validation: Code review');
|
||||
|
||||
const review = await codeReviewer.execute(
|
||||
{ userContext: state.userContext },
|
||||
state.strategyCode
|
||||
);
|
||||
|
||||
// Simple issue detection (in production, parse structured output)
|
||||
const hasIssues = review.toLowerCase().includes('critical') ||
|
||||
review.toLowerCase().includes('reject');
|
||||
|
||||
return {
|
||||
codeReview: review,
|
||||
codeIssues: hasIssues ? ['Issues detected in code review'] : [],
|
||||
needsFixing: hasIssues,
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Node: Fix Code Issues
|
||||
* Uses LLM to fix issues identified in code review
|
||||
*/
|
||||
export function createFixCodeNode(
|
||||
model: BaseChatModel,
|
||||
logger: FastifyBaseLogger
|
||||
) {
|
||||
return async (state: StrategyValidationStateType): Promise<Partial<StrategyValidationStateType>> => {
|
||||
logger.info('Strategy validation: Fixing code issues');
|
||||
|
||||
const systemPrompt = `You are a trading strategy developer.
|
||||
Fix the issues identified in the code review while maintaining the strategy's logic.
|
||||
Return only the corrected code without explanation.`;
|
||||
|
||||
const userPrompt = `Original code:
|
||||
\`\`\`typescript
|
||||
${state.strategyCode}
|
||||
\`\`\`
|
||||
|
||||
Code review feedback:
|
||||
${state.codeReview}
|
||||
|
||||
Provide the corrected code:`;
|
||||
|
||||
const response = await model.invoke([
|
||||
new SystemMessage(systemPrompt),
|
||||
new HumanMessage(userPrompt),
|
||||
]);
|
||||
|
||||
const fixedCode = (response.content as string)
|
||||
.replace(/```typescript\n?/g, '')
|
||||
.replace(/```\n?/g, '')
|
||||
.trim();
|
||||
|
||||
return {
|
||||
strategyCode: fixedCode,
|
||||
validationRetryCount: state.validationRetryCount + 1,
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Node: Backtest Strategy
|
||||
* Runs backtest using user's MCP server
|
||||
*/
|
||||
export function createBacktestNode(
|
||||
mcpBacktestFn: (code: string, ticker: string, timeframe: string) => Promise<Record<string, unknown>>,
|
||||
logger: FastifyBaseLogger
|
||||
) {
|
||||
return async (state: StrategyValidationStateType): Promise<Partial<StrategyValidationStateType>> => {
|
||||
logger.info('Strategy validation: Running backtest');
|
||||
|
||||
try {
|
||||
const results = await mcpBacktestFn(
|
||||
state.strategyCode,
|
||||
state.ticker,
|
||||
state.timeframe
|
||||
);
|
||||
|
||||
// Check if backtest passed (simplified)
|
||||
const sharpeRatio = (results.sharpeRatio as number) || 0;
|
||||
const passed = sharpeRatio > 0.5;
|
||||
|
||||
return {
|
||||
backtestResults: results,
|
||||
backtestPassed: passed,
|
||||
needsFixing: !passed,
|
||||
};
|
||||
} catch (error) {
|
||||
logger.error({ error }, 'Backtest failed');
|
||||
return {
|
||||
backtestResults: { error: (error as Error).message },
|
||||
backtestPassed: false,
|
||||
needsFixing: true,
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Node: Risk Assessment
|
||||
* Analyzes backtest results for risk
|
||||
*/
|
||||
export function createRiskAssessmentNode(
|
||||
model: BaseChatModel,
|
||||
logger: FastifyBaseLogger
|
||||
) {
|
||||
return async (state: StrategyValidationStateType): Promise<Partial<StrategyValidationStateType>> => {
|
||||
logger.info('Strategy validation: Risk assessment');
|
||||
|
||||
const systemPrompt = `You are a risk management expert.
|
||||
Analyze the strategy and backtest results to assess risk level.
|
||||
Provide: risk level (low/medium/high) and detailed assessment.`;
|
||||
|
||||
const userPrompt = `Strategy code:
|
||||
\`\`\`typescript
|
||||
${state.strategyCode}
|
||||
\`\`\`
|
||||
|
||||
Backtest results:
|
||||
${JSON.stringify(state.backtestResults, null, 2)}
|
||||
|
||||
Provide risk assessment in format:
|
||||
RISK_LEVEL: [low/medium/high]
|
||||
ASSESSMENT: [detailed explanation]`;
|
||||
|
||||
const response = await model.invoke([
|
||||
new SystemMessage(systemPrompt),
|
||||
new HumanMessage(userPrompt),
|
||||
]);
|
||||
|
||||
const assessment = response.content as string;
|
||||
|
||||
// Parse risk level (simplified)
|
||||
let riskLevel: 'low' | 'medium' | 'high' = 'medium';
|
||||
if (assessment.includes('RISK_LEVEL: low')) riskLevel = 'low';
|
||||
if (assessment.includes('RISK_LEVEL: high')) riskLevel = 'high';
|
||||
|
||||
return {
|
||||
riskAssessment: assessment,
|
||||
riskLevel,
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Node: Human Approval
|
||||
* Pauses workflow for human review
|
||||
*/
|
||||
export function createHumanApprovalNode(logger: FastifyBaseLogger) {
|
||||
return async (state: StrategyValidationStateType): Promise<Partial<StrategyValidationStateType>> => {
|
||||
logger.info('Strategy validation: Awaiting human approval');
|
||||
|
||||
// In real implementation, this would:
|
||||
// 1. Send approval request to user's channel
|
||||
// 2. Store workflow state with interrupt
|
||||
// 3. Wait for user response
|
||||
// 4. Resume with approval decision
|
||||
|
||||
// For now, auto-approve if risk is low/medium and backtest passed
|
||||
const autoApprove = state.backtestPassed &&
|
||||
(state.riskLevel === 'low' || state.riskLevel === 'medium');
|
||||
|
||||
return {
|
||||
humanApproved: autoApprove,
|
||||
approvalComment: autoApprove ? 'Auto-approved: passed validation' : 'Needs manual review',
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Node: Final Recommendation
|
||||
* Generates final recommendation based on all steps
|
||||
*/
|
||||
export function createRecommendationNode(
|
||||
model: BaseChatModel,
|
||||
logger: FastifyBaseLogger
|
||||
) {
|
||||
return async (state: StrategyValidationStateType): Promise<Partial<StrategyValidationStateType>> => {
|
||||
logger.info('Strategy validation: Generating recommendation');
|
||||
|
||||
const systemPrompt = `You are the final decision maker for strategy deployment.
|
||||
Based on all validation steps, provide a clear recommendation: approve, reject, or revise.`;
|
||||
|
||||
const userPrompt = `Strategy validation summary:
|
||||
|
||||
Code Review: ${state.codeIssues.length === 0 ? 'Passed' : 'Issues found'}
|
||||
Backtest: ${state.backtestPassed ? 'Passed' : 'Failed'}
|
||||
Risk Level: ${state.riskLevel}
|
||||
Human Approved: ${state.humanApproved}
|
||||
|
||||
Backtest Results:
|
||||
${JSON.stringify(state.backtestResults, null, 2)}
|
||||
|
||||
Risk Assessment:
|
||||
${state.riskAssessment}
|
||||
|
||||
Provide final recommendation (approve/reject/revise) and reasoning:`;
|
||||
|
||||
const response = await model.invoke([
|
||||
new SystemMessage(systemPrompt),
|
||||
new HumanMessage(userPrompt),
|
||||
]);
|
||||
|
||||
const recommendation = response.content as string;
|
||||
|
||||
// Parse recommendation (simplified)
|
||||
let decision: 'approve' | 'reject' | 'revise' = 'revise';
|
||||
if (recommendation.toLowerCase().includes('approve')) decision = 'approve';
|
||||
if (recommendation.toLowerCase().includes('reject')) decision = 'reject';
|
||||
|
||||
return {
|
||||
recommendation: decision,
|
||||
recommendationReason: recommendation,
|
||||
output: recommendation,
|
||||
};
|
||||
};
|
||||
}
|
||||
78
gateway/src/harness/workflows/strategy-validation/state.ts
Normal file
78
gateway/src/harness/workflows/strategy-validation/state.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
import { Annotation } from '@langchain/langgraph';
|
||||
import { BaseWorkflowState } from '../base-workflow.js';
|
||||
|
||||
/**
|
||||
* Strategy validation workflow state
|
||||
*
|
||||
* Extends base workflow state with strategy-specific fields
|
||||
*/
|
||||
export const StrategyValidationState = Annotation.Root({
|
||||
...BaseWorkflowState.spec,
|
||||
|
||||
// Input
|
||||
strategyCode: Annotation<string>(),
|
||||
ticker: Annotation<string>(),
|
||||
timeframe: Annotation<string>(),
|
||||
|
||||
// Code review step
|
||||
codeReview: Annotation<string | null>({
|
||||
value: (left, right) => right ?? left,
|
||||
default: () => null,
|
||||
}),
|
||||
codeIssues: Annotation<string[]>({
|
||||
value: (left, right) => right ?? left,
|
||||
default: () => [],
|
||||
}),
|
||||
|
||||
// Backtest step
|
||||
backtestResults: Annotation<Record<string, unknown> | null>({
|
||||
value: (left, right) => right ?? left,
|
||||
default: () => null,
|
||||
}),
|
||||
backtestPassed: Annotation<boolean>({
|
||||
value: (left, right) => right ?? left,
|
||||
default: () => false,
|
||||
}),
|
||||
|
||||
// Risk assessment step
|
||||
riskAssessment: Annotation<string | null>({
|
||||
value: (left, right) => right ?? left,
|
||||
default: () => null,
|
||||
}),
|
||||
riskLevel: Annotation<'low' | 'medium' | 'high' | null>({
|
||||
value: (left, right) => right ?? left,
|
||||
default: () => null,
|
||||
}),
|
||||
|
||||
// Human approval step
|
||||
humanApproved: Annotation<boolean>({
|
||||
value: (left, right) => right ?? left,
|
||||
default: () => false,
|
||||
}),
|
||||
approvalComment: Annotation<string | null>({
|
||||
value: (left, right) => right ?? left,
|
||||
default: () => null,
|
||||
}),
|
||||
|
||||
// Validation loop control
|
||||
validationRetryCount: Annotation<number>({
|
||||
value: (left, right) => right ?? left,
|
||||
default: () => 0,
|
||||
}),
|
||||
needsFixing: Annotation<boolean>({
|
||||
value: (left, right) => right ?? left,
|
||||
default: () => false,
|
||||
}),
|
||||
|
||||
// Final output
|
||||
recommendation: Annotation<'approve' | 'reject' | 'revise' | null>({
|
||||
value: (left, right) => right ?? left,
|
||||
default: () => null,
|
||||
}),
|
||||
recommendationReason: Annotation<string | null>({
|
||||
value: (left, right) => right ?? left,
|
||||
default: () => null,
|
||||
}),
|
||||
});
|
||||
|
||||
export type StrategyValidationStateType = typeof StrategyValidationState.State;
|
||||
19
gateway/src/harness/workflows/trading-request/config.yaml
Normal file
19
gateway/src/harness/workflows/trading-request/config.yaml
Normal file
@@ -0,0 +1,19 @@
|
||||
# Trading Request Workflow Configuration
|
||||
|
||||
name: trading-request
|
||||
description: Human-in-the-loop workflow for executing trading requests
|
||||
|
||||
# Workflow settings
|
||||
timeout: 600000 # 10 minutes (includes human wait time)
|
||||
maxRetries: 1
|
||||
requiresApproval: true
|
||||
approvalNodes:
|
||||
- await_approval
|
||||
|
||||
# Trading limits
|
||||
maxPositionPercent: 0.05 # 5% of portfolio max
|
||||
minRiskRewardRatio: 2.0 # Minimum 2:1 risk/reward
|
||||
|
||||
# Model override (optional)
|
||||
model: claude-3-5-sonnet-20241022
|
||||
temperature: 0.2
|
||||
229
gateway/src/harness/workflows/trading-request/graph.ts
Normal file
229
gateway/src/harness/workflows/trading-request/graph.ts
Normal file
@@ -0,0 +1,229 @@
|
||||
import { StateGraph } from '@langchain/langgraph';
|
||||
import { BaseWorkflow, type WorkflowConfig } from '../base-workflow.js';
|
||||
import { TradingRequestState, type TradingRequestStateType } from './state.js';
|
||||
import type { FastifyBaseLogger } from 'fastify';
|
||||
import type { BaseChatModel } from '@langchain/core/language_models/chat_models';
|
||||
import { HumanMessage, SystemMessage } from '@langchain/core/messages';
|
||||
|
||||
/**
|
||||
* Trading Request Workflow
|
||||
*
|
||||
* Human-in-the-loop workflow for executing trades:
|
||||
* 1. Analyze market conditions
|
||||
* 2. Calculate risk and position size
|
||||
* 3. Request human approval (PAUSE HERE)
|
||||
* 4. If approved → Execute trade
|
||||
* 5. Generate execution summary
|
||||
*
|
||||
* Features:
|
||||
* - Interrupt at approval node
|
||||
* - Resume with user input
|
||||
* - Risk validation
|
||||
* - Multi-channel approval UI
|
||||
*/
|
||||
export class TradingRequestWorkflow extends BaseWorkflow<TradingRequestStateType> {
|
||||
constructor(
|
||||
config: WorkflowConfig,
|
||||
private model: BaseChatModel,
|
||||
private marketDataFn: (ticker: string) => Promise<{ price: number; [key: string]: unknown }>,
|
||||
private executeTradeFn: (order: any) => Promise<{ orderId: string; status: string; price: number }>,
|
||||
logger: FastifyBaseLogger
|
||||
) {
|
||||
super(config, logger);
|
||||
}
|
||||
|
||||
buildGraph(): any {
|
||||
const graph = new StateGraph(TradingRequestState);
|
||||
|
||||
// Node: Analyze market
|
||||
const analyzeNode = async (state: TradingRequestStateType): Promise<Partial<TradingRequestStateType>> => {
|
||||
this.logger.info('Trading request: Analyzing market');
|
||||
|
||||
const marketData = await this.marketDataFn(state.ticker);
|
||||
|
||||
const systemPrompt = `You are a market analyst. Analyze current conditions for a ${state.side} order.`;
|
||||
const userPrompt = `Ticker: ${state.ticker}
|
||||
Current Price: ${marketData.price}
|
||||
Requested: ${state.side} ${state.amount} at ${state.price || 'market'}
|
||||
|
||||
Provide 2-3 sentence analysis:`;
|
||||
|
||||
const response = await this.model.invoke([
|
||||
new SystemMessage(systemPrompt),
|
||||
new HumanMessage(userPrompt),
|
||||
]);
|
||||
|
||||
return {
|
||||
marketAnalysis: response.content as string,
|
||||
currentPrice: marketData.price,
|
||||
};
|
||||
};
|
||||
|
||||
// Node: Calculate risk
|
||||
const calculateRiskNode = async (state: TradingRequestStateType): Promise<Partial<TradingRequestStateType>> => {
|
||||
this.logger.info('Trading request: Calculating risk');
|
||||
|
||||
// Simplified risk calculation
|
||||
const accountBalance = state.userContext.license.features.maxBacktestDays * 1000; // Mock
|
||||
const maxPosition = accountBalance * 0.05; // 5% max
|
||||
|
||||
const positionValue = state.amount * (state.currentPrice || 0);
|
||||
const positionSize = Math.min(positionValue, maxPosition);
|
||||
|
||||
// Mock risk/reward (in production, calculate from stop-loss and take-profit)
|
||||
const riskRewardRatio = 2.5;
|
||||
|
||||
return {
|
||||
riskAssessment: {
|
||||
accountBalance,
|
||||
maxPosition,
|
||||
positionValue,
|
||||
positionSize,
|
||||
},
|
||||
riskRewardRatio,
|
||||
positionSize,
|
||||
};
|
||||
};
|
||||
|
||||
// Node: Request approval (INTERRUPT POINT)
|
||||
const requestApprovalNode = async (state: TradingRequestStateType): Promise<Partial<TradingRequestStateType>> => {
|
||||
this.logger.info('Trading request: Requesting approval');
|
||||
|
||||
// TODO: Send approval request to user's active channel
|
||||
// In production, this would:
|
||||
// 1. Format approval UI for the channel (buttons for Telegram, etc.)
|
||||
// 2. Send message with trade details
|
||||
// 3. Store workflow state
|
||||
// 4. Return with interrupt signal
|
||||
// 5. LangGraph will pause here until resumed with user input
|
||||
|
||||
// For now, mock approval
|
||||
const approvalMessage = `
|
||||
Trade Request Approval Needed:
|
||||
- ${state.side.toUpperCase()} ${state.amount} ${state.ticker}
|
||||
- Current Price: $${state.currentPrice}
|
||||
- Position Size: $${state.positionSize}
|
||||
- Risk/Reward: ${state.riskRewardRatio}:1
|
||||
|
||||
Market Analysis:
|
||||
${state.marketAnalysis}
|
||||
|
||||
Reply 'approve' or 'reject'
|
||||
`;
|
||||
|
||||
return {
|
||||
approvalRequested: true,
|
||||
approvalMessage,
|
||||
approvalTimestamp: Date.now(),
|
||||
// In production, this node would use Interrupt here
|
||||
userApproved: false, // Wait for user input
|
||||
};
|
||||
};
|
||||
|
||||
// Node: Execute trade
|
||||
const executeTradeNode = async (state: TradingRequestStateType): Promise<Partial<TradingRequestStateType>> => {
|
||||
this.logger.info('Trading request: Executing trade');
|
||||
|
||||
try {
|
||||
const order = {
|
||||
ticker: state.ticker,
|
||||
side: state.side,
|
||||
amount: state.amount,
|
||||
type: state.requestType,
|
||||
price: state.price,
|
||||
};
|
||||
|
||||
const result = await this.executeTradeFn(order);
|
||||
|
||||
return {
|
||||
orderPlaced: true,
|
||||
orderId: result.orderId,
|
||||
executionPrice: result.price,
|
||||
executionStatus: result.status as any,
|
||||
};
|
||||
} catch (error) {
|
||||
this.logger.error({ error }, 'Trade execution failed');
|
||||
return {
|
||||
orderPlaced: false,
|
||||
executionStatus: 'rejected',
|
||||
error: (error as Error).message,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
// Node: Generate summary
|
||||
const summaryNode = async (state: TradingRequestStateType): Promise<Partial<TradingRequestStateType>> => {
|
||||
this.logger.info('Trading request: Generating summary');
|
||||
|
||||
const summary = state.orderPlaced
|
||||
? `Trade executed successfully:
|
||||
- Order ID: ${state.orderId}
|
||||
- ${state.side.toUpperCase()} ${state.amount} ${state.ticker}
|
||||
- Execution Price: $${state.executionPrice}
|
||||
- Status: ${state.executionStatus}`
|
||||
: `Trade not executed:
|
||||
- Reason: ${state.userApproved ? 'Execution failed' : 'User rejected'}`;
|
||||
|
||||
return {
|
||||
summary,
|
||||
output: summary,
|
||||
};
|
||||
};
|
||||
|
||||
// Add nodes
|
||||
graph
|
||||
.addNode('analyze', analyzeNode)
|
||||
.addNode('calculate_risk', calculateRiskNode)
|
||||
.addNode('request_approval', requestApprovalNode)
|
||||
.addNode('execute_trade', executeTradeNode)
|
||||
.addNode('summary', summaryNode);
|
||||
|
||||
// Define edges
|
||||
(graph as any).addEdge('__start__', 'analyze');
|
||||
(graph as any).addEdge('analyze', 'calculate_risk');
|
||||
(graph as any).addEdge('calculate_risk', 'request_approval');
|
||||
|
||||
// Conditional: After approval, execute or reject
|
||||
(graph as any).addConditionalEdges('request_approval', (state: any) => {
|
||||
// In production, this would check if user approved via interrupt resume
|
||||
return state.userApproved ? 'execute_trade' : 'summary';
|
||||
});
|
||||
|
||||
(graph as any).addEdge('execute_trade', 'summary');
|
||||
(graph as any).addEdge('summary', '__end__');
|
||||
|
||||
return graph;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory function to create and compile workflow
|
||||
*/
|
||||
export async function createTradingRequestWorkflow(
|
||||
model: BaseChatModel,
|
||||
marketDataFn: (ticker: string) => Promise<{ price: number; [key: string]: unknown }>,
|
||||
executeTradeFn: (order: any) => Promise<{ orderId: string; status: string; price: number }>,
|
||||
logger: FastifyBaseLogger,
|
||||
configPath: string
|
||||
): Promise<TradingRequestWorkflow> {
|
||||
const { readFile } = await import('fs/promises');
|
||||
const yaml = await import('js-yaml');
|
||||
|
||||
// Load config
|
||||
const configContent = await readFile(configPath, 'utf-8');
|
||||
const config = yaml.load(configContent) as WorkflowConfig;
|
||||
|
||||
// Create workflow
|
||||
const workflow = new TradingRequestWorkflow(
|
||||
config,
|
||||
model,
|
||||
marketDataFn,
|
||||
executeTradeFn,
|
||||
logger
|
||||
);
|
||||
|
||||
// Compile graph
|
||||
workflow.compile();
|
||||
|
||||
return workflow;
|
||||
}
|
||||
89
gateway/src/harness/workflows/trading-request/state.ts
Normal file
89
gateway/src/harness/workflows/trading-request/state.ts
Normal file
@@ -0,0 +1,89 @@
|
||||
import { Annotation } from '@langchain/langgraph';
|
||||
import { BaseWorkflowState } from '../base-workflow.js';
|
||||
|
||||
/**
|
||||
* Trading request workflow state
|
||||
*
|
||||
* Handles human-in-the-loop approval for trade execution
|
||||
*/
|
||||
export const TradingRequestState = Annotation.Root({
|
||||
...BaseWorkflowState.spec,
|
||||
|
||||
// Input
|
||||
requestType: Annotation<'market_order' | 'limit_order' | 'stop_loss'>(),
|
||||
ticker: Annotation<string>(),
|
||||
side: Annotation<'buy' | 'sell'>(),
|
||||
amount: Annotation<number>(), // Requested amount
|
||||
price: Annotation<number | null>({
|
||||
value: (left, right) => right ?? left,
|
||||
default: () => null,
|
||||
}),
|
||||
|
||||
// Analysis step
|
||||
marketAnalysis: Annotation<string | null>({
|
||||
value: (left, right) => right ?? left,
|
||||
default: () => null,
|
||||
}),
|
||||
currentPrice: Annotation<number | null>({
|
||||
value: (left, right) => right ?? left,
|
||||
default: () => null,
|
||||
}),
|
||||
|
||||
// Risk calculation
|
||||
riskAssessment: Annotation<Record<string, unknown> | null>({
|
||||
value: (left, right) => right ?? left,
|
||||
default: () => null,
|
||||
}),
|
||||
riskRewardRatio: Annotation<number | null>({
|
||||
value: (left, right) => right ?? left,
|
||||
default: () => null,
|
||||
}),
|
||||
positionSize: Annotation<number | null>({
|
||||
value: (left, right) => right ?? left,
|
||||
default: () => null,
|
||||
}),
|
||||
|
||||
// Human approval
|
||||
approvalRequested: Annotation<boolean>({
|
||||
value: (left, right) => right ?? left,
|
||||
default: () => false,
|
||||
}),
|
||||
userApproved: Annotation<boolean>({
|
||||
value: (left, right) => right ?? left,
|
||||
default: () => false,
|
||||
}),
|
||||
approvalMessage: Annotation<string | null>({
|
||||
value: (left, right) => right ?? left,
|
||||
default: () => null,
|
||||
}),
|
||||
approvalTimestamp: Annotation<number | null>({
|
||||
value: (left, right) => right ?? left,
|
||||
default: () => null,
|
||||
}),
|
||||
|
||||
// Execution
|
||||
orderPlaced: Annotation<boolean>({
|
||||
value: (left, right) => right ?? left,
|
||||
default: () => false,
|
||||
}),
|
||||
orderId: Annotation<string | null>({
|
||||
value: (left, right) => right ?? left,
|
||||
default: () => null,
|
||||
}),
|
||||
executionPrice: Annotation<number | null>({
|
||||
value: (left, right) => right ?? left,
|
||||
default: () => null,
|
||||
}),
|
||||
executionStatus: Annotation<'pending' | 'filled' | 'rejected' | 'cancelled' | null>({
|
||||
value: (left, right) => right ?? left,
|
||||
default: () => null,
|
||||
}),
|
||||
|
||||
// Output
|
||||
summary: Annotation<string | null>({
|
||||
value: (left, right) => right ?? left,
|
||||
default: () => null,
|
||||
}),
|
||||
});
|
||||
|
||||
export type TradingRequestStateType = typeof TradingRequestState.State;
|
||||
Reference in New Issue
Block a user