client-py connected
This commit is contained in:
@@ -101,9 +101,15 @@ See individual workflow READMEs for details.
|
||||
|
||||
YAML-based configuration:
|
||||
|
||||
- `models.yaml`: LLM providers, routing, rate limits
|
||||
- `subagent-routing.yaml`: When to use which subagent
|
||||
|
||||
Model configuration is centralized in the main gateway config (`/config/config.yaml`), including:
|
||||
- Default models and providers
|
||||
- License tier model assignments (free, pro, enterprise)
|
||||
- Model routing rules (complexity, cost-optimized)
|
||||
|
||||
Resource limits (token limits, rate limits) are stored in the database per user.
|
||||
|
||||
## User Context
|
||||
|
||||
Enhanced session context with channel awareness for multi-channel support:
|
||||
|
||||
@@ -8,6 +8,7 @@ import { MCPClientConnector } from './mcp-client.js';
|
||||
import { CONTEXT_URIS, type ResourceContent } from '../types/resources.js';
|
||||
import { LLMProviderFactory, type ProviderConfig } from '../llm/provider.js';
|
||||
import { ModelRouter, RoutingStrategy } from '../llm/router.js';
|
||||
import type { WorkspaceManager } from '../workspace/workspace-manager.js';
|
||||
|
||||
export interface AgentHarnessConfig {
|
||||
userId: string;
|
||||
@@ -15,6 +16,7 @@ export interface AgentHarnessConfig {
|
||||
license: UserLicense;
|
||||
providerConfig: ProviderConfig;
|
||||
logger: FastifyBaseLogger;
|
||||
workspaceManager?: WorkspaceManager;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -33,9 +35,13 @@ export class AgentHarness {
|
||||
private modelFactory: LLMProviderFactory;
|
||||
private modelRouter: ModelRouter;
|
||||
private mcpClient: MCPClientConnector;
|
||||
private workspaceManager?: WorkspaceManager;
|
||||
private lastWorkspaceSeq: number = 0;
|
||||
private isFirstMessage: boolean = true;
|
||||
|
||||
constructor(config: AgentHarnessConfig) {
|
||||
this.config = config;
|
||||
this.workspaceManager = config.workspaceManager;
|
||||
|
||||
this.modelFactory = new LLMProviderFactory(config.providerConfig, config.logger);
|
||||
this.modelRouter = new ModelRouter(this.modelFactory, config.logger);
|
||||
@@ -102,18 +108,14 @@ export class AgentHarness {
|
||||
// 7. Extract text response (tool handling TODO)
|
||||
const assistantMessage = response.content as string;
|
||||
|
||||
// 8. Save messages to user's MCP server
|
||||
this.config.logger.debug('Saving messages to MCP');
|
||||
await this.mcpClient.callTool('save_message', {
|
||||
role: 'user',
|
||||
content: message.content,
|
||||
timestamp: message.timestamp.toISOString(),
|
||||
});
|
||||
await this.mcpClient.callTool('save_message', {
|
||||
role: 'assistant',
|
||||
content: assistantMessage,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
// TODO: Save messages to Iceberg conversation table instead of MCP
|
||||
// Should batch-insert periodically or on session end to avoid many small Parquet files
|
||||
// await icebergConversationStore.appendMessages([...]);
|
||||
|
||||
// Mark first message as processed
|
||||
if (this.isFirstMessage) {
|
||||
this.isFirstMessage = false;
|
||||
}
|
||||
|
||||
return {
|
||||
messageId: `msg_${Date.now()}`,
|
||||
@@ -157,17 +159,17 @@ export class AgentHarness {
|
||||
yield content;
|
||||
}
|
||||
|
||||
// Save after streaming completes
|
||||
await this.mcpClient.callTool('save_message', {
|
||||
role: 'user',
|
||||
content: message.content,
|
||||
timestamp: message.timestamp.toISOString(),
|
||||
});
|
||||
await this.mcpClient.callTool('save_message', {
|
||||
role: 'assistant',
|
||||
content: fullResponse,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
// TODO: Save messages to Iceberg conversation table instead of MCP
|
||||
// Should batch-insert periodically or on session end to avoid many small Parquet files
|
||||
// await icebergConversationStore.appendMessages([
|
||||
// { role: 'user', content: message.content, timestamp: message.timestamp },
|
||||
// { role: 'assistant', content: fullResponse, timestamp: new Date() }
|
||||
// ]);
|
||||
|
||||
// Mark first message as processed
|
||||
if (this.isFirstMessage) {
|
||||
this.isFirstMessage = false;
|
||||
}
|
||||
} catch (error) {
|
||||
this.config.logger.error({ error }, 'Error streaming message');
|
||||
throw error;
|
||||
@@ -224,6 +226,15 @@ export class AgentHarness {
|
||||
});
|
||||
}
|
||||
|
||||
// Add workspace delta (for subsequent turns)
|
||||
const workspaceDelta = this.buildWorkspaceDelta();
|
||||
if (workspaceDelta) {
|
||||
messages.push({
|
||||
role: 'user',
|
||||
content: workspaceDelta,
|
||||
});
|
||||
}
|
||||
|
||||
// Add current user message
|
||||
messages.push({
|
||||
role: 'user',
|
||||
@@ -273,9 +284,18 @@ Available features: ${JSON.stringify(this.config.license.features, null, 2)}`;
|
||||
prompt += `\n\n# User Profile\n${userProfile.text}`;
|
||||
}
|
||||
|
||||
// Add workspace context
|
||||
// Add workspace context from MCP resource (if available)
|
||||
if (workspaceState?.text) {
|
||||
prompt += `\n\n# Current Workspace\n${workspaceState.text}`;
|
||||
prompt += `\n\n# Current Workspace (from MCP)\n${workspaceState.text}`;
|
||||
}
|
||||
|
||||
// Add full workspace state from WorkspaceManager (first message only)
|
||||
if (this.isFirstMessage && this.workspaceManager) {
|
||||
const workspaceJSON = this.workspaceManager.serializeState();
|
||||
prompt += `\n\n# Workspace State (JSON)\n\`\`\`json\n${workspaceJSON}\n\`\`\``;
|
||||
|
||||
// Record current workspace sequence for delta tracking
|
||||
this.lastWorkspaceSeq = this.workspaceManager.getCurrentSeq();
|
||||
}
|
||||
|
||||
// Add user's custom instructions (highest priority)
|
||||
@@ -286,6 +306,30 @@ Available features: ${JSON.stringify(this.config.license.features, null, 2)}`;
|
||||
return prompt;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build workspace delta message for subsequent turns.
|
||||
* Returns null if no changes since last message.
|
||||
*/
|
||||
private buildWorkspaceDelta(): string | null {
|
||||
if (!this.workspaceManager || this.isFirstMessage) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const changes = this.workspaceManager.getChangesSince(this.lastWorkspaceSeq);
|
||||
|
||||
if (Object.keys(changes).length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Format changes as JSON
|
||||
const deltaJSON = JSON.stringify(changes, null, 2);
|
||||
|
||||
// Update sequence marker
|
||||
this.lastWorkspaceSeq = this.workspaceManager.getCurrentSeq();
|
||||
|
||||
return `[Workspace Changes Since Last Turn]\n\`\`\`json\n${deltaJSON}\n\`\`\``;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,110 +0,0 @@
|
||||
# Default LLM Model Configuration
|
||||
|
||||
# Default model for general agent tasks
|
||||
default:
|
||||
provider: anthropic
|
||||
model: claude-3-5-sonnet-20241022
|
||||
temperature: 0.7
|
||||
maxTokens: 4096
|
||||
|
||||
# Model overrides for specific use cases
|
||||
models:
|
||||
# Fast model for simple tasks (routing, classification)
|
||||
fast:
|
||||
provider: anthropic
|
||||
model: claude-3-haiku-20240307
|
||||
temperature: 0.3
|
||||
maxTokens: 1024
|
||||
|
||||
# Reasoning model for complex analysis
|
||||
reasoning:
|
||||
provider: anthropic
|
||||
model: claude-3-5-sonnet-20241022
|
||||
temperature: 0.5
|
||||
maxTokens: 8192
|
||||
|
||||
# Precise model for code generation/review
|
||||
code:
|
||||
provider: anthropic
|
||||
model: claude-3-5-sonnet-20241022
|
||||
temperature: 0.2
|
||||
maxTokens: 8192
|
||||
|
||||
# Creative model for strategy brainstorming
|
||||
creative:
|
||||
provider: anthropic
|
||||
model: claude-3-5-sonnet-20241022
|
||||
temperature: 0.9
|
||||
maxTokens: 4096
|
||||
|
||||
# Embedding model configuration
|
||||
embeddings:
|
||||
provider: openai
|
||||
model: text-embedding-3-small
|
||||
dimensions: 1536
|
||||
|
||||
# Model routing rules (complexity-based)
|
||||
routing:
|
||||
# Simple queries → fast model
|
||||
simple:
|
||||
keywords:
|
||||
- "what is"
|
||||
- "define"
|
||||
- "list"
|
||||
- "show me"
|
||||
maxTokens: 100
|
||||
model: fast
|
||||
|
||||
# Code-related → code model
|
||||
code:
|
||||
keywords:
|
||||
- "code"
|
||||
- "function"
|
||||
- "implement"
|
||||
- "debug"
|
||||
- "review"
|
||||
model: code
|
||||
|
||||
# Analysis tasks → reasoning model
|
||||
analysis:
|
||||
keywords:
|
||||
- "analyze"
|
||||
- "compare"
|
||||
- "evaluate"
|
||||
- "assess"
|
||||
model: reasoning
|
||||
|
||||
# Everything else → default
|
||||
default:
|
||||
model: default
|
||||
|
||||
# Cost optimization settings
|
||||
costControl:
|
||||
# Cache system prompts (Anthropic prompt caching)
|
||||
cacheSystemPrompts: true
|
||||
|
||||
# Token limits per license type
|
||||
tokenLimits:
|
||||
free:
|
||||
maxTokensPerMessage: 2048
|
||||
maxTokensPerDay: 50000
|
||||
pro:
|
||||
maxTokensPerMessage: 8192
|
||||
maxTokensPerDay: 500000
|
||||
enterprise:
|
||||
maxTokensPerMessage: 16384
|
||||
maxTokensPerDay: -1 # unlimited
|
||||
|
||||
# Rate limiting
|
||||
rateLimits:
|
||||
# Requests per minute by license
|
||||
requestsPerMinute:
|
||||
free: 10
|
||||
pro: 60
|
||||
enterprise: 120
|
||||
|
||||
# Concurrent requests
|
||||
concurrentRequests:
|
||||
free: 1
|
||||
pro: 3
|
||||
enterprise: 10
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
||||
import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js';
|
||||
|
||||
import type { FastifyBaseLogger } from 'fastify';
|
||||
|
||||
@@ -11,7 +12,7 @@ export interface MCPClientConfig {
|
||||
|
||||
/**
|
||||
* MCP client connector for user's container
|
||||
* Manages connection to user-specific MCP server
|
||||
* Manages connection to user-specific MCP server via SSE transport
|
||||
*/
|
||||
export class MCPClientConnector {
|
||||
private client: Client | null = null;
|
||||
@@ -23,8 +24,7 @@ export class MCPClientConnector {
|
||||
}
|
||||
|
||||
/**
|
||||
* Connect to user's MCP server
|
||||
* TODO: Implement HTTP/SSE transport instead of stdio for container communication
|
||||
* Connect to user's MCP server via SSE transport
|
||||
*/
|
||||
async connect(): Promise<void> {
|
||||
if (this.connected) {
|
||||
@@ -34,7 +34,7 @@ export class MCPClientConnector {
|
||||
try {
|
||||
this.config.logger.info(
|
||||
{ userId: this.config.userId, url: this.config.mcpServerUrl },
|
||||
'Connecting to user MCP server'
|
||||
'Connecting to user MCP server via SSE'
|
||||
);
|
||||
|
||||
this.client = new Client(
|
||||
@@ -46,24 +46,18 @@ export class MCPClientConnector {
|
||||
capabilities: {
|
||||
sampling: {},
|
||||
},
|
||||
} as any
|
||||
}
|
||||
);
|
||||
|
||||
// TODO: Replace with HTTP transport when user containers are ready
|
||||
// For now, this is a placeholder structure
|
||||
// const transport = new HTTPTransport(this.config.mcpServerUrl, {
|
||||
// headers: {
|
||||
// 'Authorization': `Bearer ${this.config.platformJWT}`
|
||||
// }
|
||||
// });
|
||||
|
||||
// Placeholder: will be replaced with actual container transport
|
||||
this.config.logger.warn(
|
||||
'MCP transport not yet implemented - using placeholder'
|
||||
// Create SSE transport for HTTP connection to user container
|
||||
const transport = new SSEClientTransport(
|
||||
new URL(`${this.config.mcpServerUrl}/sse`)
|
||||
);
|
||||
|
||||
await this.client.connect(transport);
|
||||
|
||||
this.connected = true;
|
||||
this.config.logger.info('Connected to user MCP server');
|
||||
this.config.logger.info('Connected to user MCP server via SSE');
|
||||
} catch (error) {
|
||||
this.config.logger.error(
|
||||
{ error, userId: this.config.userId },
|
||||
@@ -84,12 +78,8 @@ export class MCPClientConnector {
|
||||
try {
|
||||
this.config.logger.debug({ tool: name, args }, 'Calling MCP tool');
|
||||
|
||||
// TODO: Implement when MCP client is connected
|
||||
// const result = await this.client.callTool({ name, arguments: args });
|
||||
// return result;
|
||||
|
||||
// Placeholder response
|
||||
return { success: true, message: 'MCP tool call placeholder' };
|
||||
const result = await this.client.callTool({ name, arguments: args });
|
||||
return result;
|
||||
} catch (error) {
|
||||
this.config.logger.error({ error, tool: name }, 'MCP tool call failed');
|
||||
throw error;
|
||||
@@ -98,27 +88,35 @@ export class MCPClientConnector {
|
||||
|
||||
/**
|
||||
* List available tools from user's MCP server
|
||||
* Filters to only return tools marked as agent_accessible
|
||||
*/
|
||||
async listTools(): Promise<Array<{ name: string; description?: string }>> {
|
||||
async listTools(): Promise<Array<{ name: string; description?: string; inputSchema?: any }>> {
|
||||
if (!this.client || !this.connected) {
|
||||
throw new Error('MCP client not connected');
|
||||
}
|
||||
|
||||
try {
|
||||
// TODO: Implement when MCP client is connected
|
||||
// const tools = await this.client.listTools();
|
||||
// return tools;
|
||||
const response = await this.client.listTools();
|
||||
|
||||
// Placeholder tools (actions only, not context)
|
||||
return [
|
||||
{ name: 'save_message', description: 'Save message to conversation history' },
|
||||
{ name: 'list_strategies', description: 'List user strategies' },
|
||||
{ name: 'read_strategy', description: 'Read strategy code' },
|
||||
{ name: 'write_strategy', description: 'Write strategy code' },
|
||||
{ name: 'run_backtest', description: 'Run backtest on strategy' },
|
||||
{ name: 'get_watchlist', description: 'Get user watchlist' },
|
||||
{ name: 'execute_trade', description: 'Execute trade' },
|
||||
];
|
||||
// Filter tools to only include agent-accessible ones
|
||||
const tools = response.tools
|
||||
.filter((tool: any) => {
|
||||
// Check if tool has agent_accessible annotation
|
||||
const annotations = tool.annotations || {};
|
||||
return annotations.agent_accessible === true;
|
||||
})
|
||||
.map((tool: any) => ({
|
||||
name: tool.name,
|
||||
description: tool.description,
|
||||
inputSchema: tool.inputSchema,
|
||||
}));
|
||||
|
||||
this.config.logger.debug(
|
||||
{ totalTools: response.tools.length, agentAccessibleTools: tools.length },
|
||||
'Listed MCP tools with filtering'
|
||||
);
|
||||
|
||||
return tools;
|
||||
} catch (error) {
|
||||
this.config.logger.error({ error }, 'Failed to list MCP tools');
|
||||
throw error;
|
||||
@@ -127,6 +125,7 @@ export class MCPClientConnector {
|
||||
|
||||
/**
|
||||
* List available resources from user's MCP server
|
||||
* Filters to only return resources marked as agent_accessible
|
||||
*/
|
||||
async listResources(): Promise<Array<{ uri: string; name: string; description?: string; mimeType?: string }>> {
|
||||
if (!this.client || !this.connected) {
|
||||
@@ -134,37 +133,28 @@ export class MCPClientConnector {
|
||||
}
|
||||
|
||||
try {
|
||||
// TODO: Implement when MCP client is connected
|
||||
// const resources = await this.client.listResources();
|
||||
// return resources;
|
||||
const response = await this.client.listResources();
|
||||
|
||||
// Placeholder resources for user context
|
||||
return [
|
||||
{
|
||||
uri: 'context://user-profile',
|
||||
name: 'User Profile',
|
||||
description: 'User trading style, preferences, and background',
|
||||
mimeType: 'text/plain',
|
||||
},
|
||||
{
|
||||
uri: 'context://conversation-summary',
|
||||
name: 'Conversation Summary',
|
||||
description: 'Semantic summary of recent conversation history with RAG',
|
||||
mimeType: 'text/plain',
|
||||
},
|
||||
{
|
||||
uri: 'context://workspace-state',
|
||||
name: 'Workspace State',
|
||||
description: 'Current chart, watchlist, and open positions',
|
||||
mimeType: 'application/json',
|
||||
},
|
||||
{
|
||||
uri: 'context://system-prompt',
|
||||
name: 'Custom System Prompt',
|
||||
description: 'User custom instructions for the assistant',
|
||||
mimeType: 'text/plain',
|
||||
},
|
||||
];
|
||||
// Filter resources to only include agent-accessible ones
|
||||
const resources = response.resources
|
||||
.filter((resource: any) => {
|
||||
// Check if resource has agent_accessible annotation
|
||||
const annotations = resource.annotations || {};
|
||||
return annotations.agent_accessible === true;
|
||||
})
|
||||
.map((resource: any) => ({
|
||||
uri: resource.uri,
|
||||
name: resource.name,
|
||||
description: resource.description,
|
||||
mimeType: resource.mimeType,
|
||||
}));
|
||||
|
||||
this.config.logger.debug(
|
||||
{ totalResources: response.resources.length, agentAccessibleResources: resources.length },
|
||||
'Listed MCP resources with filtering'
|
||||
);
|
||||
|
||||
return resources;
|
||||
} catch (error) {
|
||||
this.config.logger.error({ error }, 'Failed to list MCP resources');
|
||||
throw error;
|
||||
@@ -182,55 +172,18 @@ export class MCPClientConnector {
|
||||
try {
|
||||
this.config.logger.debug({ uri }, 'Reading MCP resource');
|
||||
|
||||
// TODO: Implement when MCP client is connected
|
||||
// const resource = await this.client.readResource({ uri });
|
||||
// return resource;
|
||||
const response = await this.client.readResource({ uri });
|
||||
|
||||
// Placeholder resource content
|
||||
if (uri === 'context://user-profile') {
|
||||
return {
|
||||
uri,
|
||||
mimeType: 'text/plain',
|
||||
text: `User Profile:
|
||||
- Trading experience: Intermediate
|
||||
- Preferred timeframes: 1h, 4h, 1d
|
||||
- Risk tolerance: Medium
|
||||
- Focus: Swing trading with technical indicators`,
|
||||
};
|
||||
} else if (uri === 'context://conversation-summary') {
|
||||
return {
|
||||
uri,
|
||||
mimeType: 'text/plain',
|
||||
text: `Recent Conversation Summary:
|
||||
[RAG-generated summary would go here]
|
||||
// Extract the first content item (MCP returns array of contents)
|
||||
const content = response.contents[0];
|
||||
|
||||
User recently discussed:
|
||||
- Moving average crossover strategies
|
||||
- Backtesting on BTC/USDT
|
||||
- Risk management techniques`,
|
||||
};
|
||||
} else if (uri === 'context://workspace-state') {
|
||||
return {
|
||||
uri,
|
||||
mimeType: 'application/json',
|
||||
text: JSON.stringify({
|
||||
currentChart: { ticker: 'BINANCE:BTC/USDT', timeframe: '1h' },
|
||||
watchlist: ['BTC/USDT', 'ETH/USDT', 'SOL/USDT'],
|
||||
openPositions: [],
|
||||
}, null, 2),
|
||||
};
|
||||
} else if (uri === 'context://system-prompt') {
|
||||
return {
|
||||
uri,
|
||||
mimeType: 'text/plain',
|
||||
text: `Custom Instructions:
|
||||
- Be concise and data-driven
|
||||
- Always show risk/reward ratios
|
||||
- Prefer simple strategies over complex ones`,
|
||||
};
|
||||
}
|
||||
|
||||
return { uri, text: '' };
|
||||
// Handle union type: content is either TextContent or BlobContent
|
||||
return {
|
||||
uri: content.uri,
|
||||
mimeType: content.mimeType,
|
||||
text: 'text' in content ? content.text : undefined,
|
||||
blob: 'blob' in content ? content.blob : undefined,
|
||||
};
|
||||
} catch (error) {
|
||||
this.config.logger.error({ error, uri }, 'MCP resource read failed');
|
||||
throw error;
|
||||
|
||||
@@ -42,7 +42,7 @@ name: my-subagent
|
||||
description: What it does
|
||||
|
||||
# Model override (optional)
|
||||
model: claude-3-5-sonnet-20241022
|
||||
model: claude-sonnet-4-6
|
||||
temperature: 0.3
|
||||
maxTokens: 4096
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ name: code-reviewer
|
||||
description: Reviews trading strategy code for bugs, performance issues, and best practices
|
||||
|
||||
# Model configuration (optional override)
|
||||
model: claude-3-5-sonnet-20241022
|
||||
model: claude-sonnet-4-6
|
||||
temperature: 0.3
|
||||
maxTokens: 4096
|
||||
|
||||
|
||||
@@ -334,7 +334,7 @@ timeout: 60000
|
||||
maxRetries: 3
|
||||
requiresApproval: false
|
||||
|
||||
model: claude-3-5-sonnet-20241022
|
||||
model: claude-sonnet-4-6
|
||||
```
|
||||
|
||||
### 6. Add Factory Function
|
||||
|
||||
@@ -15,5 +15,5 @@ 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
|
||||
model: claude-sonnet-4-6
|
||||
temperature: 0.3
|
||||
|
||||
@@ -15,5 +15,5 @@ maxPositionPercent: 0.05 # 5% of portfolio max
|
||||
minRiskRewardRatio: 2.0 # Minimum 2:1 risk/reward
|
||||
|
||||
# Model override (optional)
|
||||
model: claude-3-5-sonnet-20241022
|
||||
model: claude-sonnet-4-6
|
||||
temperature: 0.2
|
||||
|
||||
Reference in New Issue
Block a user