redesign fully scaffolded and web login works

This commit is contained in:
2026-03-17 20:10:47 -04:00
parent b9cc397e05
commit f6bd22a8ef
143 changed files with 17317 additions and 693 deletions

View 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)

View 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 || [];
}
}

View 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';

View File

@@ -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

View 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;
}

View 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,
};
};
}

View 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;

View 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

View 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;
}

View 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;