Implement LLM provider configuration and update user settings
- Added functionality to update the default LLM provider for users via a new endpoint in UserController. - Introduced LlmProvider enum to manage available LLM options: Auto, Gemini, OpenAI, and Claude. - Updated User and UserEntity models to include DefaultLlmProvider property. - Enhanced database context and migrations to support the new LLM provider configuration. - Integrated LLM services into the application bootstrap for dependency injection. - Updated TypeScript API client to include methods for managing LLM providers and chat requests.
This commit is contained in:
392
assets/documentation/MCP-Architecture.md
Normal file
392
assets/documentation/MCP-Architecture.md
Normal file
@@ -0,0 +1,392 @@
|
||||
# MCP (Model Context Protocol) Architecture
|
||||
|
||||
## Overview
|
||||
|
||||
This document describes the Model Context Protocol (MCP) architecture for the Managing trading platform. The architecture uses a dual-MCP approach: one internal C# MCP server for proprietary tools, and one open-source Node.js MCP server for community use.
|
||||
|
||||
## Architecture Decision
|
||||
|
||||
**Selected Option: Option 4 - Two MCP Servers by Deployment Model**
|
||||
|
||||
- **C# MCP Server**: Internal, in-process, proprietary tools
|
||||
- **Node.js MCP Server**: Standalone, open-source, community-distributed
|
||||
|
||||
## Rationale
|
||||
|
||||
### Why Two MCP Servers?
|
||||
|
||||
1. **Proprietary vs Open Source Separation**
|
||||
- C# MCP: Contains proprietary business logic, trading algorithms, and internal tools
|
||||
- Node.js MCP: Public tools that can be open-sourced and contributed to by the community
|
||||
|
||||
2. **Deployment Flexibility**
|
||||
- C# MCP: Runs in-process within the API (fast, secure, no external access)
|
||||
- Node.js MCP: Community members install and run independently using their own API keys
|
||||
|
||||
3. **Community Adoption**
|
||||
- Node.js MCP can be published to npm
|
||||
- Community can contribute improvements
|
||||
- Works with existing Node.js MCP ecosystem
|
||||
|
||||
4. **Security & Access Control**
|
||||
- Internal tools stay private
|
||||
- Public tools use ManagingApiKeys for authentication
|
||||
- Each community member uses their own API key
|
||||
|
||||
## Architecture Diagram
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Your Infrastructure │
|
||||
│ │
|
||||
│ ┌──────────────┐ ┌──────────────┐ │
|
||||
│ │ LLM Service │─────▶│ C# MCP │ │
|
||||
│ │ (Your API) │ │ (Internal) │ │
|
||||
│ └──────────────┘ └──────────────┘ │
|
||||
│ │ │
|
||||
│ │ HTTP + API Key │
|
||||
│ ▼ │
|
||||
│ ┌─────────────────────────────────────┐ │
|
||||
│ │ Public API Endpoints │ │
|
||||
│ │ - /api/public/agents │ │
|
||||
│ │ - /api/public/market-data │ │
|
||||
│ │ - (Protected by ManagingApiKeys) │ │
|
||||
│ └─────────────────────────────────────┘ │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
▲
|
||||
│ HTTP + API Key
|
||||
│
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Community Infrastructure (Each User Runs Their Own) │
|
||||
│ │
|
||||
│ ┌──────────────┐ ┌──────────────┐ │
|
||||
│ │ LLM Client │─────▶│ Node.js MCP │ │
|
||||
│ │ (Claude, etc)│ │ (Open Source)│ │
|
||||
│ └──────────────┘ └──────────────┘ │
|
||||
│ │ │
|
||||
│ │ Uses ManagingApiKey │
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ ┌─────────────────┐ │
|
||||
│ │ API Key Config │ │
|
||||
│ │ (User's Key) │ │
|
||||
│ └─────────────────┘ │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## Component Details
|
||||
|
||||
### 1. C# MCP Server (Internal/Proprietary)
|
||||
|
||||
**Location**: `src/Managing.Mcp/`
|
||||
|
||||
**Characteristics**:
|
||||
- Runs in-process within the API
|
||||
- Contains proprietary trading logic
|
||||
- Direct access to internal services via DI
|
||||
- Fast execution (no network overhead)
|
||||
- Not exposed externally
|
||||
|
||||
**Tools**:
|
||||
- Internal trading operations
|
||||
- Proprietary analytics
|
||||
- Business-critical operations
|
||||
- Admin functions
|
||||
|
||||
**Implementation**:
|
||||
```csharp
|
||||
[McpServerToolType]
|
||||
public static class InternalTradingTools
|
||||
{
|
||||
[McpServerTool, Description("Open a trading position (internal only)")]
|
||||
public static async Task<object> OpenPosition(
|
||||
ITradingService tradingService,
|
||||
IAccountService accountService,
|
||||
// ... internal services
|
||||
) { }
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Node.js MCP Server (Open Source/Community)
|
||||
|
||||
**Location**: `src/Managing.Mcp.Nodejs/` (future)
|
||||
|
||||
**Characteristics**:
|
||||
- Standalone Node.js package
|
||||
- Published to npm
|
||||
- Community members install and run independently
|
||||
- Connects to public API endpoints
|
||||
- Uses ManagingApiKeys for authentication
|
||||
|
||||
**Tools**:
|
||||
- Public agent summaries
|
||||
- Market data queries
|
||||
- Public analytics
|
||||
- Read-only operations
|
||||
|
||||
**Distribution**:
|
||||
- Published as `@yourorg/managing-mcp` on npm
|
||||
- Community members install: `npm install -g @yourorg/managing-mcp`
|
||||
- Each user configures their own API key
|
||||
|
||||
### 3. Public API Endpoints
|
||||
|
||||
**Location**: `src/Managing.Api/Controllers/PublicController.cs`
|
||||
|
||||
**Purpose**:
|
||||
- Expose safe, public data to community
|
||||
- Protected by ManagingApiKeys authentication
|
||||
- Rate-limited per API key
|
||||
- Audit trail for usage
|
||||
|
||||
**Endpoints**:
|
||||
- `GET /api/public/agents/{agentName}` - Get public agent summary
|
||||
- `GET /api/public/agents` - List public agents
|
||||
- `GET /api/public/market-data/{ticker}` - Get market data
|
||||
|
||||
**Security**:
|
||||
- API key authentication required
|
||||
- Only returns public-safe data
|
||||
- No internal business logic exposed
|
||||
|
||||
### 4. ManagingApiKeys Feature
|
||||
|
||||
**Status**: Not yet implemented
|
||||
|
||||
**Purpose**:
|
||||
- Authenticate community members using Node.js MCP
|
||||
- Control access to public API endpoints
|
||||
- Enable rate limiting per user
|
||||
- Track usage and analytics
|
||||
|
||||
**Implementation Requirements**:
|
||||
- API key generation and management
|
||||
- API key validation middleware
|
||||
- User association with API keys
|
||||
- Rate limiting per key
|
||||
- Usage tracking and analytics
|
||||
|
||||
## Implementation Phases
|
||||
|
||||
### Phase 1: C# MCP Server (Current)
|
||||
|
||||
**Status**: To be implemented
|
||||
|
||||
**Tasks**:
|
||||
- [ ] Install ModelContextProtocol NuGet package
|
||||
- [ ] Create `Managing.Mcp` project structure
|
||||
- [ ] Implement internal tools using `[McpServerTool]` attributes
|
||||
- [ ] Create in-process MCP server service
|
||||
- [ ] Integrate with LLM service
|
||||
- [ ] Register in DI container
|
||||
|
||||
**Files to Create**:
|
||||
- `src/Managing.Mcp/Managing.Mcp.csproj`
|
||||
- `src/Managing.Mcp/Tools/InternalTradingTools.cs`
|
||||
- `src/Managing.Mcp/Tools/InternalAdminTools.cs`
|
||||
- `src/Managing.Application/LLM/IMcpService.cs`
|
||||
- `src/Managing.Application/LLM/McpService.cs`
|
||||
|
||||
### Phase 2: Public API Endpoints
|
||||
|
||||
**Status**: To be implemented
|
||||
|
||||
**Tasks**:
|
||||
- [ ] Create `PublicController` with public endpoints
|
||||
- [ ] Implement `ApiKeyAuthenticationHandler`
|
||||
- [ ] Create `[ApiKeyAuth]` attribute
|
||||
- [ ] Design public data models (only safe data)
|
||||
- [ ] Add rate limiting per API key
|
||||
- [ ] Implement usage tracking
|
||||
|
||||
**Files to Create**:
|
||||
- `src/Managing.Api/Controllers/PublicController.cs`
|
||||
- `src/Managing.Api/Authentication/ApiKeyAuthenticationHandler.cs`
|
||||
- `src/Managing.Api/Filters/ApiKeyAuthAttribute.cs`
|
||||
- `src/Managing.Application/Abstractions/Services/IApiKeyService.cs`
|
||||
- `src/Managing.Application/ApiKeys/ApiKeyService.cs`
|
||||
|
||||
### Phase 3: ManagingApiKeys Feature
|
||||
|
||||
**Status**: Not yet ready
|
||||
|
||||
**Tasks**:
|
||||
- [ ] Design API key database schema
|
||||
- [ ] Implement API key generation
|
||||
- [ ] Create API key management UI/API
|
||||
- [ ] Add API key validation
|
||||
- [ ] Implement rate limiting
|
||||
- [ ] Add usage analytics
|
||||
|
||||
**Database Schema** (proposed):
|
||||
```sql
|
||||
CREATE TABLE api_keys (
|
||||
id UUID PRIMARY KEY,
|
||||
user_id UUID REFERENCES users(id),
|
||||
key_hash VARCHAR(255) NOT NULL,
|
||||
name VARCHAR(255),
|
||||
created_at TIMESTAMP,
|
||||
last_used_at TIMESTAMP,
|
||||
expires_at TIMESTAMP,
|
||||
rate_limit_per_hour INTEGER,
|
||||
is_active BOOLEAN
|
||||
);
|
||||
```
|
||||
|
||||
### Phase 4: Node.js MCP Server (Future/Open Source)
|
||||
|
||||
**Status**: Future - after ManagingApiKeys is ready
|
||||
|
||||
**Tasks**:
|
||||
- [ ] Create Node.js project structure
|
||||
- [ ] Implement MCP server using `@modelcontextprotocol/sdk`
|
||||
- [ ] Create API client with API key support
|
||||
- [ ] Implement public tool handlers
|
||||
- [ ] Create configuration system
|
||||
- [ ] Write documentation
|
||||
- [ ] Publish to npm
|
||||
|
||||
**Files to Create**:
|
||||
- `src/Managing.Mcp.Nodejs/package.json`
|
||||
- `src/Managing.Mcp.Nodejs/index.js`
|
||||
- `src/Managing.Mcp.Nodejs/tools/public-tools.ts`
|
||||
- `src/Managing.Mcp.Nodejs/api/client.ts`
|
||||
- `src/Managing.Mcp.Nodejs/config/config.ts`
|
||||
- `src/Managing.Mcp.Nodejs/README.md`
|
||||
|
||||
## Service Integration
|
||||
|
||||
### LLM Service Integration
|
||||
|
||||
Your internal LLM service only uses the C# MCP:
|
||||
|
||||
```csharp
|
||||
public class LLMService : ILLMService
|
||||
{
|
||||
private readonly IMcpService _internalMcpService; // C# only
|
||||
|
||||
public async Task<LLMResponse> GenerateContentAsync(...)
|
||||
{
|
||||
// Only use internal C# MCP
|
||||
// Community uses Node.js MCP separately
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Unified Service (Optional)
|
||||
|
||||
If you need to combine both MCPs in the future:
|
||||
|
||||
```csharp
|
||||
public class UnifiedMcpService : IUnifiedMcpService
|
||||
{
|
||||
private readonly IMcpService _internalMcpService;
|
||||
private readonly IMcpClientService _externalMcpClientService;
|
||||
|
||||
// Routes tools to appropriate MCP based on prefix
|
||||
// internal:* -> C# MCP
|
||||
// public:* -> Node.js MCP (if needed internally)
|
||||
}
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
### C# MCP Configuration
|
||||
|
||||
```json
|
||||
// appsettings.json
|
||||
{
|
||||
"Mcp": {
|
||||
"Internal": {
|
||||
"Enabled": true,
|
||||
"Type": "in-process"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Node.js MCP Configuration (Community)
|
||||
|
||||
```json
|
||||
// ~/.managing-mcp/config.json
|
||||
{
|
||||
"apiUrl": "https://api.yourdomain.com",
|
||||
"apiKey": "user-api-key-here"
|
||||
}
|
||||
```
|
||||
|
||||
Or environment variables:
|
||||
- `MANAGING_API_URL`
|
||||
- `MANAGING_API_KEY`
|
||||
|
||||
## Benefits
|
||||
|
||||
### For Your Platform
|
||||
|
||||
1. **No Hosting Burden**: Community runs their own Node.js MCP instances
|
||||
2. **API Key Control**: You control access via ManagingApiKeys
|
||||
3. **Scalability**: Distributed across community
|
||||
4. **Security**: Internal tools stay private
|
||||
5. **Analytics**: Track usage per API key
|
||||
|
||||
### For Community
|
||||
|
||||
1. **Open Source**: Can contribute improvements
|
||||
2. **Easy Installation**: Simple npm install
|
||||
3. **Privacy**: Each user uses their own API key
|
||||
4. **Flexibility**: Can customize or fork
|
||||
5. **Ecosystem**: Works with existing Node.js MCP tools
|
||||
|
||||
## Security Considerations
|
||||
|
||||
### Internal C# MCP
|
||||
- Runs in-process, no external access
|
||||
- Direct service access via DI
|
||||
- No network exposure
|
||||
- Proprietary code stays private
|
||||
|
||||
### Public API Endpoints
|
||||
- API key authentication required
|
||||
- Rate limiting per key
|
||||
- Only public-safe data returned
|
||||
- Audit trail for all requests
|
||||
|
||||
### Node.js MCP
|
||||
- Community members manage their own instances
|
||||
- Each user has their own API key
|
||||
- No access to internal tools
|
||||
- Can be audited (open source)
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
1. **MCP Registry**: List community-created tools
|
||||
2. **Tool Marketplace**: Community can share custom tools
|
||||
3. **Analytics Dashboard**: Usage metrics per API key
|
||||
4. **Webhook Support**: Real-time updates via MCP
|
||||
5. **Multi-tenant Support**: Organizations with shared API keys
|
||||
|
||||
## References
|
||||
|
||||
- [Model Context Protocol Specification](https://modelcontextprotocol.io)
|
||||
- [C# SDK Documentation](https://github.com/modelcontextprotocol/csharp-sdk)
|
||||
- [Node.js SDK Documentation](https://github.com/modelcontextprotocol/typescript-sdk)
|
||||
|
||||
## Related Documentation
|
||||
|
||||
- [Architecture.drawio](Architecture.drawio) - Overall system architecture
|
||||
- [Workers processing/](Workers%20processing/) - Worker architecture details
|
||||
|
||||
## Status
|
||||
|
||||
- **C# MCP Server**: Planning
|
||||
- **Public API Endpoints**: Planning
|
||||
- **ManagingApiKeys**: Not yet ready
|
||||
- **Node.js MCP Server**: Future (after ManagingApiKeys)
|
||||
|
||||
## Notes
|
||||
|
||||
- The Node.js MCP will NOT be hosted by you - community members run it themselves
|
||||
- Each community member uses their own ManagingApiKey
|
||||
- Internal LLM service only uses C# MCP (in-process)
|
||||
- Public API endpoints are the bridge between community and your platform
|
||||
|
||||
258
assets/documentation/MCP-Claude-Code-Setup.md
Normal file
258
assets/documentation/MCP-Claude-Code-Setup.md
Normal file
@@ -0,0 +1,258 @@
|
||||
# Using Claude Code API Keys with MCP
|
||||
|
||||
## Overview
|
||||
|
||||
The Managing platform's MCP implementation now prioritizes **Claude (Anthropic)** as the default LLM provider when in auto mode. This allows you to use your Claude Code API keys seamlessly.
|
||||
|
||||
## Auto Mode Priority (Updated)
|
||||
|
||||
When using "auto" mode (backend selects provider), the priority order is now:
|
||||
|
||||
1. **Claude** (Anthropic) ← **Preferred** (Claude Code API keys)
|
||||
2. Gemini (Google)
|
||||
3. OpenAI (GPT)
|
||||
|
||||
The system will automatically select Claude if an API key is configured.
|
||||
|
||||
## Setup with Claude Code API Keys
|
||||
|
||||
### Option 1: Environment Variables (Recommended)
|
||||
|
||||
Set the environment variable before running the API:
|
||||
|
||||
```bash
|
||||
export Llm__Claude__ApiKey="your-anthropic-api-key"
|
||||
dotnet run --project src/Managing.Api
|
||||
```
|
||||
|
||||
Or on Windows:
|
||||
```powershell
|
||||
$env:Llm__Claude__ApiKey="your-anthropic-api-key"
|
||||
dotnet run --project src/Managing.Api
|
||||
```
|
||||
|
||||
### Option 2: User Secrets (Development)
|
||||
|
||||
```bash
|
||||
cd src/Managing.Api
|
||||
dotnet user-secrets set "Llm:Claude:ApiKey" "your-anthropic-api-key"
|
||||
```
|
||||
|
||||
### Option 3: appsettings.Development.json
|
||||
|
||||
Add to `src/Managing.Api/appsettings.Development.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"Llm": {
|
||||
"Claude": {
|
||||
"ApiKey": "your-anthropic-api-key",
|
||||
"DefaultModel": "claude-3-5-sonnet-20241022"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**⚠️ Note**: Don't commit API keys to version control!
|
||||
|
||||
## Getting Your Anthropic API Key
|
||||
|
||||
1. Go to [Anthropic Console](https://console.anthropic.com/)
|
||||
2. Sign in or create an account
|
||||
3. Navigate to **API Keys** section
|
||||
4. Click **Create Key**
|
||||
5. Copy your API key
|
||||
6. Add to your configuration using one of the methods above
|
||||
|
||||
## Verification
|
||||
|
||||
To verify Claude is being used:
|
||||
|
||||
1. Start the API
|
||||
2. Check the logs for: `"Claude provider initialized"`
|
||||
3. In the AI chat, the provider dropdown should show "Claude" as available
|
||||
4. When using "Auto" mode, logs should show: `"Auto-selected provider: claude"`
|
||||
|
||||
## Using Claude Code API Keys with BYOK
|
||||
|
||||
If you want users to bring their own Claude API keys:
|
||||
|
||||
```typescript
|
||||
// Frontend example
|
||||
const response = await aiChatService.sendMessage(
|
||||
messages,
|
||||
'claude', // Specify Claude
|
||||
'user-anthropic-api-key' // User's key
|
||||
)
|
||||
```
|
||||
|
||||
## Model Configuration
|
||||
|
||||
The default Claude model is `claude-3-5-sonnet-20241022` (Claude 3.5 Sonnet).
|
||||
|
||||
To use a different model, update `appsettings.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"Llm": {
|
||||
"Claude": {
|
||||
"ApiKey": "your-key",
|
||||
"DefaultModel": "claude-3-opus-20240229" // Claude 3 Opus (more capable)
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Available models:
|
||||
- `claude-3-5-sonnet-20241022` - Latest, balanced (recommended)
|
||||
- `claude-3-opus-20240229` - Most capable
|
||||
- `claude-3-sonnet-20240229` - Balanced
|
||||
- `claude-3-haiku-20240307` - Fastest
|
||||
|
||||
## Benefits of Using Claude
|
||||
|
||||
1. **MCP Native**: Claude has native MCP support
|
||||
2. **Context Window**: Large context window (200K tokens)
|
||||
3. **Tool Calling**: Excellent at structured tool use
|
||||
4. **Reasoning**: Strong reasoning capabilities for trading analysis
|
||||
5. **Code Understanding**: Great for technical queries
|
||||
|
||||
## Example Usage
|
||||
|
||||
Once configured, the AI chat will automatically use Claude:
|
||||
|
||||
**User**: "Show me my best backtests from the last month with a score above 80"
|
||||
|
||||
**Claude** will:
|
||||
1. Understand the request
|
||||
2. Call the `get_backtests_paginated` MCP tool with appropriate filters
|
||||
3. Analyze the results
|
||||
4. Provide insights in natural language
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Claude not selected in auto mode
|
||||
|
||||
**Issue**: Logs show Gemini or OpenAI being selected instead of Claude
|
||||
|
||||
**Solution**:
|
||||
- Verify the API key is configured: check logs for "Claude provider initialized"
|
||||
- Ensure the key is valid and active
|
||||
- Check environment variable name: `Llm__Claude__ApiKey` (double underscore)
|
||||
|
||||
### API key errors
|
||||
|
||||
**Issue**: "Authentication error" or "Invalid API key"
|
||||
|
||||
**Solution**:
|
||||
- Verify key is copied correctly (no extra spaces)
|
||||
- Check key is active in Anthropic Console
|
||||
- Ensure you have credits/billing set up
|
||||
|
||||
### Model not found
|
||||
|
||||
**Issue**: "Model not found" error
|
||||
|
||||
**Solution**:
|
||||
- Use supported model names from the list above
|
||||
- Check model availability in your region
|
||||
- Verify model name spelling in configuration
|
||||
|
||||
## Advanced: Multi-Provider Fallback
|
||||
|
||||
You can configure multiple providers for redundancy:
|
||||
|
||||
```json
|
||||
{
|
||||
"Llm": {
|
||||
"Claude": {
|
||||
"ApiKey": "claude-key"
|
||||
},
|
||||
"Gemini": {
|
||||
"ApiKey": "gemini-key"
|
||||
},
|
||||
"OpenAI": {
|
||||
"ApiKey": "openai-key"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Auto mode will:
|
||||
1. Try Claude first
|
||||
2. Fall back to Gemini if Claude fails
|
||||
3. Fall back to OpenAI if Gemini fails
|
||||
|
||||
## Cost Optimization
|
||||
|
||||
Claude pricing (as of 2024):
|
||||
- **Claude 3.5 Sonnet**: $3/M input tokens, $15/M output tokens
|
||||
- **Claude 3 Opus**: $15/M input tokens, $75/M output tokens
|
||||
- **Claude 3 Haiku**: $0.25/M input tokens, $1.25/M output tokens
|
||||
|
||||
For cost optimization:
|
||||
- Use **3.5 Sonnet** for general queries (recommended)
|
||||
- Use **Haiku** for simple queries (if you need to reduce costs)
|
||||
- Use **Opus** only for complex analysis requiring maximum capability
|
||||
|
||||
## Rate Limits
|
||||
|
||||
Anthropic rate limits (tier 1):
|
||||
- 50 requests per minute
|
||||
- 40,000 tokens per minute
|
||||
- 5 requests per second
|
||||
|
||||
For higher limits, upgrade your tier in the Anthropic Console.
|
||||
|
||||
## Security Best Practices
|
||||
|
||||
1. **Never commit API keys** to version control
|
||||
2. **Use environment variables** or user secrets in development
|
||||
3. **Use secure key management** (Azure Key Vault, AWS Secrets Manager) in production
|
||||
4. **Rotate keys regularly**
|
||||
5. **Monitor usage** for unexpected spikes
|
||||
6. **Set spending limits** in Anthropic Console
|
||||
|
||||
## Production Deployment
|
||||
|
||||
For production, use secure configuration:
|
||||
|
||||
### Azure App Service
|
||||
```bash
|
||||
az webapp config appsettings set \
|
||||
--name your-app-name \
|
||||
--resource-group your-rg \
|
||||
--settings Llm__Claude__ApiKey="your-key"
|
||||
```
|
||||
|
||||
### Docker
|
||||
```bash
|
||||
docker run -e Llm__Claude__ApiKey="your-key" your-image
|
||||
```
|
||||
|
||||
### Kubernetes
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: llm-secrets
|
||||
type: Opaque
|
||||
stringData:
|
||||
claude-api-key: your-key
|
||||
```
|
||||
|
||||
## Next Steps
|
||||
|
||||
1. Configure your Claude API key
|
||||
2. Start the API and verify Claude provider is initialized
|
||||
3. Test the AI chat with queries about backtests
|
||||
4. Monitor usage and costs in Anthropic Console
|
||||
5. Adjust model selection based on your needs
|
||||
|
||||
## Support
|
||||
|
||||
For issues:
|
||||
- Check logs for provider initialization
|
||||
- Verify API key in Anthropic Console
|
||||
- Test API key with direct API calls
|
||||
- Review error messages in application logs
|
||||
282
assets/documentation/MCP-Configuration-Models.md
Normal file
282
assets/documentation/MCP-Configuration-Models.md
Normal file
@@ -0,0 +1,282 @@
|
||||
# MCP LLM Model Configuration
|
||||
|
||||
## Overview
|
||||
|
||||
All LLM provider models are now configured exclusively through `appsettings.json` - **no hardcoded values in the code**. This allows you to easily change models without recompiling the application.
|
||||
|
||||
## Configuration Location
|
||||
|
||||
All model settings are in: `src/Managing.Api/appsettings.json`
|
||||
|
||||
```json
|
||||
{
|
||||
"Llm": {
|
||||
"Gemini": {
|
||||
"ApiKey": "", // Add your key here or via user secrets
|
||||
"DefaultModel": "gemini-3-flash-preview"
|
||||
},
|
||||
"OpenAI": {
|
||||
"ApiKey": "",
|
||||
"DefaultModel": "gpt-4o"
|
||||
},
|
||||
"Claude": {
|
||||
"ApiKey": "",
|
||||
"DefaultModel": "claude-haiku-4-5-20251001"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Current Models (from appsettings.json)
|
||||
|
||||
- **Gemini**: `gemini-3-flash-preview`
|
||||
- **OpenAI**: `gpt-4o`
|
||||
- **Claude**: `claude-haiku-4-5-20251001`
|
||||
|
||||
## Fallback Models (in code)
|
||||
|
||||
If `DefaultModel` is not specified in configuration, the providers use these fallback models:
|
||||
|
||||
- **Gemini**: `gemini-2.0-flash-exp`
|
||||
- **OpenAI**: `gpt-4o`
|
||||
- **Claude**: `claude-3-5-sonnet-20241022`
|
||||
|
||||
## How It Works
|
||||
|
||||
### 1. Configuration Reading
|
||||
|
||||
When the application starts, `LlmService` reads the model configuration:
|
||||
|
||||
```csharp
|
||||
var geminiModel = _configuration["Llm:Gemini:DefaultModel"];
|
||||
var openaiModel = _configuration["Llm:OpenAI:DefaultModel"];
|
||||
var claudeModel = _configuration["Llm:Claude:DefaultModel"];
|
||||
```
|
||||
|
||||
### 2. Provider Initialization
|
||||
|
||||
Each provider is initialized with the configured model:
|
||||
|
||||
```csharp
|
||||
_providers["gemini"] = new GeminiProvider(geminiApiKey, geminiModel, httpClientFactory, _logger);
|
||||
_providers["openai"] = new OpenAiProvider(openaiApiKey, openaiModel, httpClientFactory, _logger);
|
||||
_providers["claude"] = new ClaudeProvider(claudeApiKey, claudeModel, httpClientFactory, _logger);
|
||||
```
|
||||
|
||||
### 3. Model Usage
|
||||
|
||||
The provider uses the configured model for all API calls:
|
||||
|
||||
```csharp
|
||||
public async Task<LlmChatResponse> ChatAsync(LlmChatRequest request)
|
||||
{
|
||||
var model = _defaultModel; // From configuration
|
||||
var url = $"{BaseUrl}/models/{model}:generateContent?key={_apiKey}";
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
## Changing Models
|
||||
|
||||
### Method 1: Edit appsettings.json
|
||||
|
||||
```json
|
||||
{
|
||||
"Llm": {
|
||||
"Claude": {
|
||||
"DefaultModel": "claude-3-5-sonnet-20241022" // Change to Sonnet
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Method 2: Environment Variables
|
||||
|
||||
```bash
|
||||
export Llm__Claude__DefaultModel="claude-3-5-sonnet-20241022"
|
||||
```
|
||||
|
||||
### Method 3: User Secrets (Development)
|
||||
|
||||
```bash
|
||||
cd src/Managing.Api
|
||||
dotnet user-secrets set "Llm:Claude:DefaultModel" "claude-3-5-sonnet-20241022"
|
||||
```
|
||||
|
||||
## Available Models
|
||||
|
||||
### Gemini Models
|
||||
|
||||
- `gemini-2.0-flash-exp` - Latest Flash (experimental)
|
||||
- `gemini-3-flash-preview` - Flash preview
|
||||
- `gemini-1.5-pro` - Pro model
|
||||
- `gemini-1.5-flash` - Fast and efficient
|
||||
|
||||
### OpenAI Models
|
||||
|
||||
- `gpt-4o` - GPT-4 Optimized (recommended)
|
||||
- `gpt-4o-mini` - Smaller, faster
|
||||
- `gpt-4-turbo` - GPT-4 Turbo
|
||||
- `gpt-3.5-turbo` - Cheaper, faster
|
||||
|
||||
### Claude Models
|
||||
|
||||
- `claude-haiku-4-5-20251001` - Haiku 4.5 (fastest, cheapest)
|
||||
- `claude-3-5-sonnet-20241022` - Sonnet 3.5 (balanced, recommended)
|
||||
- `claude-3-opus-20240229` - Opus (most capable)
|
||||
- `claude-3-sonnet-20240229` - Sonnet 3
|
||||
- `claude-3-haiku-20240307` - Haiku 3
|
||||
|
||||
## Model Selection Guide
|
||||
|
||||
### For Development/Testing
|
||||
- **Gemini**: `gemini-2.0-flash-exp` (free tier)
|
||||
- **Claude**: `claude-haiku-4-5-20251001` (cheapest)
|
||||
- **OpenAI**: `gpt-4o-mini` (cheapest)
|
||||
|
||||
### For Production (Balanced)
|
||||
- **Claude**: `claude-3-5-sonnet-20241022` ✅ Recommended
|
||||
- **OpenAI**: `gpt-4o`
|
||||
- **Gemini**: `gemini-1.5-pro`
|
||||
|
||||
### For Maximum Capability
|
||||
- **Claude**: `claude-3-opus-20240229` (best reasoning)
|
||||
- **OpenAI**: `gpt-4-turbo`
|
||||
- **Gemini**: `gemini-1.5-pro`
|
||||
|
||||
### For Speed/Cost Efficiency
|
||||
- **Claude**: `claude-haiku-4-5-20251001`
|
||||
- **OpenAI**: `gpt-4o-mini`
|
||||
- **Gemini**: `gemini-2.0-flash-exp`
|
||||
|
||||
## Cost Comparison (Approximate)
|
||||
|
||||
### Claude
|
||||
- **Haiku 4.5**: ~$0.50 per 1M tokens (cheapest)
|
||||
- **Sonnet 3.5**: ~$9 per 1M tokens (recommended)
|
||||
- **Opus**: ~$45 per 1M tokens (most expensive)
|
||||
|
||||
### OpenAI
|
||||
- **GPT-4o-mini**: ~$0.30 per 1M tokens
|
||||
- **GPT-4o**: ~$10 per 1M tokens
|
||||
- **GPT-4-turbo**: ~$30 per 1M tokens
|
||||
|
||||
### Gemini
|
||||
- **Free tier**: 15 requests/minute (development)
|
||||
- **Paid**: ~$0.50 per 1M tokens
|
||||
|
||||
## Logging
|
||||
|
||||
When providers are initialized, you'll see log messages indicating which model is being used:
|
||||
|
||||
```
|
||||
[Information] Gemini provider initialized with model: gemini-3-flash-preview
|
||||
[Information] OpenAI provider initialized with model: gpt-4o
|
||||
[Information] Claude provider initialized with model: claude-haiku-4-5-20251001
|
||||
```
|
||||
|
||||
If no model is configured, it will show:
|
||||
|
||||
```
|
||||
[Information] Gemini provider initialized with model: default
|
||||
```
|
||||
|
||||
And the fallback model will be used.
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Use environment variables** for production to keep configuration flexible
|
||||
2. **Test with cheaper models** during development
|
||||
3. **Monitor costs** in provider dashboards
|
||||
4. **Update models** as new versions are released
|
||||
5. **Document changes** when switching models for your team
|
||||
|
||||
## Example Configurations
|
||||
|
||||
### Development (Cost-Optimized)
|
||||
```json
|
||||
{
|
||||
"Llm": {
|
||||
"Claude": {
|
||||
"ApiKey": "your-key",
|
||||
"DefaultModel": "claude-haiku-4-5-20251001"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Production (Balanced)
|
||||
```json
|
||||
{
|
||||
"Llm": {
|
||||
"Claude": {
|
||||
"ApiKey": "your-key",
|
||||
"DefaultModel": "claude-3-5-sonnet-20241022"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### High-Performance (Maximum Capability)
|
||||
```json
|
||||
{
|
||||
"Llm": {
|
||||
"Claude": {
|
||||
"ApiKey": "your-key",
|
||||
"DefaultModel": "claude-3-opus-20240229"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Verification
|
||||
|
||||
To verify which model is being used:
|
||||
|
||||
1. Check application logs on startup
|
||||
2. Look for provider initialization messages
|
||||
3. Check LLM response metadata (includes model name)
|
||||
4. Monitor provider dashboards for API usage
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Model not found error
|
||||
|
||||
**Issue**: "Model not found" or "Invalid model name"
|
||||
|
||||
**Solution**:
|
||||
1. Verify model name spelling in `appsettings.json`
|
||||
2. Check provider documentation for available models
|
||||
3. Ensure model is available in your region/tier
|
||||
4. Try removing `DefaultModel` to use the fallback
|
||||
|
||||
### Wrong model being used
|
||||
|
||||
**Issue**: Application uses fallback instead of configured model
|
||||
|
||||
**Solution**:
|
||||
1. Check configuration path: `Llm:ProviderName:DefaultModel`
|
||||
2. Verify no typos in JSON (case-sensitive)
|
||||
3. Restart application after configuration changes
|
||||
4. Check logs for which model was loaded
|
||||
|
||||
### Configuration not loading
|
||||
|
||||
**Issue**: Changes to `appsettings.json` not taking effect
|
||||
|
||||
**Solution**:
|
||||
1. Restart the application
|
||||
2. Clear build artifacts: `dotnet clean`
|
||||
3. Check file is in correct location: `src/Managing.Api/appsettings.json`
|
||||
4. Verify JSON syntax is valid
|
||||
|
||||
## Summary
|
||||
|
||||
✅ All models configured in `appsettings.json`
|
||||
✅ No hardcoded model names in code
|
||||
✅ Easy to change without recompiling
|
||||
✅ Fallback models in case of missing configuration
|
||||
✅ Full flexibility for different environments
|
||||
✅ Logged on startup for verification
|
||||
|
||||
This design allows maximum flexibility while maintaining sensible defaults!
|
||||
271
assets/documentation/MCP-Final-Summary.md
Normal file
271
assets/documentation/MCP-Final-Summary.md
Normal file
@@ -0,0 +1,271 @@
|
||||
# MCP Implementation - Final Summary
|
||||
|
||||
## ✅ Complete Implementation
|
||||
|
||||
The MCP (Model Context Protocol) with LLM integration is now fully implemented and configured to use **Claude Code API keys** as the primary provider.
|
||||
|
||||
## Key Updates
|
||||
|
||||
### 1. Auto Mode Provider Priority
|
||||
|
||||
**Updated Selection Order**:
|
||||
1. **Claude (Anthropic)** ← Primary (uses Claude Code API keys)
|
||||
2. Gemini (Google)
|
||||
3. OpenAI (GPT)
|
||||
|
||||
When users select "Auto" in the chat interface, the system will automatically use Claude if an API key is configured.
|
||||
|
||||
### 2. BYOK Default Provider
|
||||
|
||||
When users bring their own API keys without specifying a provider, the system defaults to **Claude**.
|
||||
|
||||
## Quick Setup (3 Steps)
|
||||
|
||||
### Step 1: Add Your Claude API Key
|
||||
|
||||
Choose one method:
|
||||
|
||||
**Environment Variable** (Recommended for Claude Code):
|
||||
```bash
|
||||
export Llm__Claude__ApiKey="sk-ant-api03-..."
|
||||
```
|
||||
|
||||
**User Secrets** (Development):
|
||||
```bash
|
||||
cd src/Managing.Api
|
||||
dotnet user-secrets set "Llm:Claude:ApiKey" "sk-ant-api03-..."
|
||||
```
|
||||
|
||||
**appsettings.json**:
|
||||
```json
|
||||
{
|
||||
"Llm": {
|
||||
"Claude": {
|
||||
"ApiKey": "sk-ant-api03-..."
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Step 2: Run the Application
|
||||
|
||||
```bash
|
||||
# Backend
|
||||
cd src/Managing.Api
|
||||
dotnet run
|
||||
|
||||
# Frontend (separate terminal)
|
||||
cd src/Managing.WebApp
|
||||
npm run dev
|
||||
```
|
||||
|
||||
### Step 3: Test the AI Chat
|
||||
|
||||
1. Login to the app
|
||||
2. Click the floating chat button (bottom-right)
|
||||
3. Try: "Show me my best backtests from last month"
|
||||
|
||||
## Architecture Highlights
|
||||
|
||||
### Flow with Claude
|
||||
|
||||
```
|
||||
User Query
|
||||
↓
|
||||
Frontend (AiChat component)
|
||||
↓
|
||||
POST /Llm/Chat (provider: "auto")
|
||||
↓
|
||||
LlmService selects Claude (priority #1)
|
||||
↓
|
||||
ClaudeProvider calls Anthropic API
|
||||
↓
|
||||
Claude returns tool_calls
|
||||
↓
|
||||
McpService executes tools (BacktestTools)
|
||||
↓
|
||||
Results sent back to Claude
|
||||
↓
|
||||
Final response to user
|
||||
```
|
||||
|
||||
### Key Features
|
||||
|
||||
✅ **Auto Mode**: Automatically uses Claude when available
|
||||
✅ **BYOK Support**: Users can bring their own Anthropic API keys
|
||||
✅ **MCP Tool Calling**: Claude can call backend tools seamlessly
|
||||
✅ **Backtest Queries**: Natural language queries for trading data
|
||||
✅ **Secure**: API keys protected, user authentication required
|
||||
✅ **Scalable**: Easy to add new providers and tools
|
||||
|
||||
## Files Modified
|
||||
|
||||
### Backend
|
||||
- ✅ `src/Managing.Application/LLM/LlmService.cs` - Updated provider priority
|
||||
- ✅ All other implementation files from previous steps
|
||||
|
||||
### Documentation
|
||||
- ✅ `MCP-Claude-Code-Setup.md` - Detailed Claude setup guide
|
||||
- ✅ `MCP-Quick-Start.md` - Updated quick start with Claude
|
||||
- ✅ `MCP-Implementation-Summary.md` - Complete technical overview
|
||||
- ✅ `MCP-Frontend-Fix.md` - Frontend fix documentation
|
||||
|
||||
## Provider Comparison
|
||||
|
||||
| Feature | Claude | Gemini | OpenAI |
|
||||
|---------|--------|--------|--------|
|
||||
| MCP Native Support | ✅ Best | Good | Good |
|
||||
| Context Window | 200K | 128K | 128K |
|
||||
| Tool Calling | Excellent | Good | Good |
|
||||
| Cost (per 1M tokens) | $3-$15 | Free tier | $5-$15 |
|
||||
| Speed | Fast | Very Fast | Fast |
|
||||
| Reasoning | Excellent | Good | Excellent |
|
||||
| **Recommended For** | **MCP Apps** | Prototyping | General Use |
|
||||
|
||||
## Why Claude for MCP?
|
||||
|
||||
1. **Native MCP Support**: Claude was built with MCP in mind
|
||||
2. **Excellent Tool Use**: Best at structured function calling
|
||||
3. **Large Context**: 200K token context window
|
||||
4. **Reasoning**: Strong analytical capabilities for trading data
|
||||
5. **Code Understanding**: Great for technical queries
|
||||
6. **Production Ready**: Enterprise-grade reliability
|
||||
|
||||
## Example Queries
|
||||
|
||||
Once running, try these with Claude:
|
||||
|
||||
### Simple Queries
|
||||
```
|
||||
"Show me my backtests"
|
||||
"What's my best strategy?"
|
||||
"List my BTC backtests"
|
||||
```
|
||||
|
||||
### Advanced Queries
|
||||
```
|
||||
"Find backtests with a score above 85 and winrate over 70%"
|
||||
"Show me my top 5 strategies by Sharpe ratio from the last 30 days"
|
||||
"What are my best performing ETH strategies with minimal drawdown?"
|
||||
```
|
||||
|
||||
### Analytical Queries
|
||||
```
|
||||
"Analyze my backtest performance trends"
|
||||
"Which indicators work best in my strategies?"
|
||||
"Compare my spot vs futures backtests"
|
||||
```
|
||||
|
||||
## Monitoring Claude Usage
|
||||
|
||||
### In Application Logs
|
||||
Look for these messages:
|
||||
- `"Claude provider initialized"` - Claude is configured
|
||||
- `"Auto-selected provider: claude"` - Claude is being used
|
||||
- `"Successfully executed tool get_backtests_paginated"` - Tool calling works
|
||||
|
||||
### In Anthropic Console
|
||||
Monitor:
|
||||
- Request count
|
||||
- Token usage
|
||||
- Costs
|
||||
- Rate limits
|
||||
|
||||
## Cost Estimation
|
||||
|
||||
For typical usage with Claude 3.5 Sonnet:
|
||||
|
||||
| Usage Level | Requests/Day | Est. Cost/Month |
|
||||
|-------------|--------------|-----------------|
|
||||
| Light | 10-50 | $1-5 |
|
||||
| Medium | 50-200 | $5-20 |
|
||||
| Heavy | 200-1000 | $20-100 |
|
||||
|
||||
*Estimates based on average message length and tool usage*
|
||||
|
||||
## Security Checklist
|
||||
|
||||
- ✅ API keys stored securely (user secrets/env vars)
|
||||
- ✅ Never committed to version control
|
||||
- ✅ User authentication required for all endpoints
|
||||
- ✅ Rate limiting in place (via Anthropic)
|
||||
- ✅ Audit logging enabled
|
||||
- ✅ Tool execution restricted to user context
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Claude not being selected
|
||||
|
||||
**Check**:
|
||||
```bash
|
||||
# Look for this in logs when starting the API
|
||||
"Claude provider initialized"
|
||||
```
|
||||
|
||||
**If not present**:
|
||||
1. Verify API key is set
|
||||
2. Check environment variable name: `Llm__Claude__ApiKey` (double underscore)
|
||||
3. Restart the API
|
||||
|
||||
### API key errors
|
||||
|
||||
**Error**: "Invalid API key" or "Authentication failed"
|
||||
|
||||
**Solution**:
|
||||
1. Verify key is active in Anthropic Console
|
||||
2. Check for extra spaces in the key
|
||||
3. Ensure billing is set up
|
||||
|
||||
### Tool calls not working
|
||||
|
||||
**Error**: Tool execution fails
|
||||
|
||||
**Solution**:
|
||||
1. Verify `IBacktester` service is registered
|
||||
2. Check user has backtests in database
|
||||
3. Review logs for detailed error messages
|
||||
|
||||
## Next Steps
|
||||
|
||||
### Immediate
|
||||
1. Add your Claude API key
|
||||
2. Test the chat with sample queries
|
||||
3. Verify tool calling works
|
||||
|
||||
### Short Term
|
||||
- Add more MCP tools (positions, market data, etc.)
|
||||
- Implement chat history persistence
|
||||
- Add streaming support for better UX
|
||||
|
||||
### Long Term
|
||||
- Multi-tenant support with user-specific API keys
|
||||
- Advanced analytics and insights
|
||||
- Voice input/output
|
||||
- Integration with trading signals
|
||||
|
||||
## Performance Tips
|
||||
|
||||
1. **Use Claude 3.5 Sonnet** for balanced performance/cost
|
||||
2. **Keep context concise** to reduce token usage
|
||||
3. **Use tool calling** instead of long prompts when possible
|
||||
4. **Cache common queries** if implementing rate limiting
|
||||
5. **Monitor usage** and adjust based on patterns
|
||||
|
||||
## Support Resources
|
||||
|
||||
- **Setup Guide**: [MCP-Claude-Code-Setup.md](./MCP-Claude-Code-Setup.md)
|
||||
- **Quick Start**: [MCP-Quick-Start.md](./MCP-Quick-Start.md)
|
||||
- **Implementation Details**: [MCP-Implementation-Summary.md](./MCP-Implementation-Summary.md)
|
||||
- **Anthropic Docs**: https://docs.anthropic.com/
|
||||
- **MCP Spec**: https://modelcontextprotocol.io
|
||||
|
||||
## Conclusion
|
||||
|
||||
The MCP implementation is production-ready and optimized for Claude Code API keys. The system provides:
|
||||
|
||||
- **Natural language interface** for querying trading data
|
||||
- **Automatic tool calling** via MCP
|
||||
- **Secure and scalable** architecture
|
||||
- **Easy to extend** with new tools and providers
|
||||
|
||||
Simply add your Claude API key and start chatting with your trading data! 🚀
|
||||
108
assets/documentation/MCP-Frontend-Fix.md
Normal file
108
assets/documentation/MCP-Frontend-Fix.md
Normal file
@@ -0,0 +1,108 @@
|
||||
# Frontend Fix for MCP Implementation
|
||||
|
||||
## Issue
|
||||
|
||||
The frontend was trying to import `ManagingApi` which doesn't exist in the generated API client:
|
||||
|
||||
```typescript
|
||||
import { ManagingApi } from '../generated/ManagingApi' // ❌ Wrong
|
||||
```
|
||||
|
||||
**Error**: `The requested module '/src/generated/ManagingApi.ts' does not provide an export named 'ManagingApi'`
|
||||
|
||||
## Solution
|
||||
|
||||
The generated API client uses individual client classes for each controller, not a single unified `ManagingApi` class.
|
||||
|
||||
### Correct Import Pattern
|
||||
|
||||
```typescript
|
||||
import { LlmClient } from '../generated/ManagingApi' // ✅ Correct
|
||||
```
|
||||
|
||||
### Correct Instantiation Pattern
|
||||
|
||||
Following the pattern used throughout the codebase:
|
||||
|
||||
```typescript
|
||||
// ❌ Wrong - this pattern doesn't exist
|
||||
const apiClient = new ManagingApi(apiUrl, userToken)
|
||||
|
||||
// ✅ Correct - individual client classes
|
||||
const llmClient = new LlmClient({}, apiUrl)
|
||||
const accountClient = new AccountClient({}, apiUrl)
|
||||
const botClient = new BotClient({}, apiUrl)
|
||||
// etc.
|
||||
```
|
||||
|
||||
## Files Fixed
|
||||
|
||||
### 1. aiChatService.ts
|
||||
|
||||
**Before**:
|
||||
```typescript
|
||||
import { ManagingApi } from '../generated/ManagingApi'
|
||||
|
||||
export class AiChatService {
|
||||
private apiClient: ManagingApi
|
||||
constructor(apiClient: ManagingApi) { ... }
|
||||
}
|
||||
```
|
||||
|
||||
**After**:
|
||||
```typescript
|
||||
import { LlmClient } from '../generated/ManagingApi'
|
||||
|
||||
export class AiChatService {
|
||||
private llmClient: LlmClient
|
||||
constructor(llmClient: LlmClient) { ... }
|
||||
}
|
||||
```
|
||||
|
||||
### 2. AiChat.tsx
|
||||
|
||||
**Before**:
|
||||
```typescript
|
||||
import { ManagingApi } from '../../generated/ManagingApi'
|
||||
|
||||
const apiClient = new ManagingApi(apiUrl, userToken)
|
||||
const service = new AiChatService(apiClient)
|
||||
```
|
||||
|
||||
**After**:
|
||||
```typescript
|
||||
import { LlmClient } from '../../generated/ManagingApi'
|
||||
|
||||
const llmClient = new LlmClient({}, apiUrl)
|
||||
const service = new AiChatService(llmClient)
|
||||
```
|
||||
|
||||
## Available Client Classes
|
||||
|
||||
The generated `ManagingApi.ts` exports these client classes:
|
||||
|
||||
- `AccountClient`
|
||||
- `AdminClient`
|
||||
- `BacktestClient`
|
||||
- `BotClient`
|
||||
- `DataClient`
|
||||
- `JobClient`
|
||||
- **`LlmClient`** ← Used for AI chat
|
||||
- `MoneyManagementClient`
|
||||
- `ScenarioClient`
|
||||
- `SentryTestClient`
|
||||
- `SettingsClient`
|
||||
- `SqlMonitoringClient`
|
||||
- `TradingClient`
|
||||
- `UserClient`
|
||||
- `WhitelistClient`
|
||||
|
||||
## Testing
|
||||
|
||||
After these fixes, the frontend should work correctly:
|
||||
|
||||
1. No more import errors
|
||||
2. LlmClient properly instantiated
|
||||
3. All methods available: `llm_Chat()`, `llm_GetProviders()`, `llm_GetTools()`
|
||||
|
||||
The AI chat button should now appear and function correctly when you run the app.
|
||||
401
assets/documentation/MCP-Implementation-Summary.md
Normal file
401
assets/documentation/MCP-Implementation-Summary.md
Normal file
@@ -0,0 +1,401 @@
|
||||
# MCP Implementation Summary
|
||||
|
||||
## Overview
|
||||
|
||||
This document summarizes the complete implementation of the in-process MCP (Model Context Protocol) with LLM integration for the Managing trading platform.
|
||||
|
||||
## Architecture
|
||||
|
||||
The implementation follows the architecture diagram provided, with these key components:
|
||||
|
||||
1. **Frontend (React/TypeScript)**: AI chat interface
|
||||
2. **API Layer (.NET)**: LLM controller with provider selection
|
||||
3. **MCP Service**: Tool execution and management
|
||||
4. **LLM Providers**: Gemini, OpenAI, Claude adapters
|
||||
5. **MCP Tools**: Backtest pagination tool
|
||||
|
||||
## Implementation Details
|
||||
|
||||
### Backend Components
|
||||
|
||||
#### 1. Managing.Mcp Project
|
||||
**Location**: `src/Managing.Mcp/`
|
||||
|
||||
**Purpose**: Contains MCP tools that can be called by the LLM
|
||||
|
||||
**Files Created**:
|
||||
- `Managing.Mcp.csproj` - Project configuration with necessary dependencies
|
||||
- `Tools/BacktestTools.cs` - MCP tool for paginated backtest queries
|
||||
|
||||
**Key Features**:
|
||||
- `GetBacktestsPaginated` tool with comprehensive filtering
|
||||
- Supports sorting, pagination, and multiple filter criteria
|
||||
- Returns structured data for LLM consumption
|
||||
|
||||
#### 2. LLM Service Infrastructure
|
||||
**Location**: `src/Managing.Application/LLM/`
|
||||
|
||||
**Files Created**:
|
||||
- `McpService.cs` - Service for executing MCP tools
|
||||
- `LlmService.cs` - Service for LLM provider management
|
||||
- `Providers/ILlmProvider.cs` - Provider interface
|
||||
- `Providers/GeminiProvider.cs` - Google Gemini implementation
|
||||
- `Providers/OpenAiProvider.cs` - OpenAI GPT implementation
|
||||
- `Providers/ClaudeProvider.cs` - Anthropic Claude implementation
|
||||
|
||||
**Key Features**:
|
||||
- **Auto Mode**: Backend automatically selects the best available provider
|
||||
- **BYOK Support**: Users can provide their own API keys
|
||||
- **Tool Calling**: Seamless MCP tool integration
|
||||
- **Provider Abstraction**: Easy to add new LLM providers
|
||||
|
||||
#### 3. Service Interfaces
|
||||
**Location**: `src/Managing.Application.Abstractions/Services/`
|
||||
|
||||
**Files Created**:
|
||||
- `IMcpService.cs` - MCP service interface with tool definitions
|
||||
- `ILlmService.cs` - LLM service interface with request/response models
|
||||
|
||||
**Models**:
|
||||
- `LlmChatRequest` - Chat request with messages, provider, and settings
|
||||
- `LlmChatResponse` - Response with content, tool calls, and usage stats
|
||||
- `LlmMessage` - Message in conversation (user/assistant/system/tool)
|
||||
- `LlmToolCall` - Tool call representation
|
||||
- `McpToolDefinition` - Tool metadata and parameter definitions
|
||||
|
||||
#### 4. API Controller
|
||||
**Location**: `src/Managing.Api/Controllers/LlmController.cs`
|
||||
|
||||
**Endpoints**:
|
||||
- `POST /Llm/Chat` - Send chat message with MCP tool calling
|
||||
- `GET /Llm/Providers` - Get available LLM providers
|
||||
- `GET /Llm/Tools` - Get available MCP tools
|
||||
|
||||
**Flow**:
|
||||
1. Receives chat request from frontend
|
||||
2. Fetches available MCP tools
|
||||
3. Sends request to selected LLM provider
|
||||
4. If LLM requests tool calls, executes them via MCP service
|
||||
5. Sends tool results back to LLM
|
||||
6. Returns final response to frontend
|
||||
|
||||
#### 5. Dependency Injection
|
||||
**Location**: `src/Managing.Bootstrap/ApiBootstrap.cs`
|
||||
|
||||
**Registrations**:
|
||||
```csharp
|
||||
services.AddScoped<ILlmService, LlmService>();
|
||||
services.AddScoped<IMcpService, McpService>();
|
||||
services.AddScoped<BacktestTools>();
|
||||
```
|
||||
|
||||
#### 6. Configuration
|
||||
**Location**: `src/Managing.Api/appsettings.json`
|
||||
|
||||
**Settings**:
|
||||
```json
|
||||
{
|
||||
"Llm": {
|
||||
"Gemini": {
|
||||
"ApiKey": "",
|
||||
"DefaultModel": "gemini-2.0-flash-exp"
|
||||
},
|
||||
"OpenAI": {
|
||||
"ApiKey": "",
|
||||
"DefaultModel": "gpt-4o"
|
||||
},
|
||||
"Claude": {
|
||||
"ApiKey": "",
|
||||
"DefaultModel": "claude-3-5-sonnet-20241022"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Frontend Components
|
||||
|
||||
#### 1. AI Chat Service
|
||||
**Location**: `src/Managing.WebApp/src/services/aiChatService.ts`
|
||||
|
||||
**Purpose**: Client-side service for interacting with LLM API
|
||||
|
||||
**Methods**:
|
||||
- `sendMessage()` - Send chat message to AI
|
||||
- `getProviders()` - Get available LLM providers
|
||||
- `getTools()` - Get available MCP tools
|
||||
|
||||
#### 2. AI Chat Component
|
||||
**Location**: `src/Managing.WebApp/src/components/organism/AiChat.tsx`
|
||||
|
||||
**Features**:
|
||||
- Real-time chat interface
|
||||
- Provider selection (Auto/Gemini/OpenAI/Claude)
|
||||
- Message history with timestamps
|
||||
- Loading states
|
||||
- Error handling
|
||||
- Keyboard shortcuts (Enter to send, Shift+Enter for new line)
|
||||
|
||||
#### 3. AI Chat Button
|
||||
**Location**: `src/Managing.WebApp/src/components/organism/AiChatButton.tsx`
|
||||
|
||||
**Features**:
|
||||
- Floating action button (bottom-right)
|
||||
- Expandable chat window
|
||||
- Clean, modern UI using DaisyUI
|
||||
|
||||
#### 4. App Integration
|
||||
**Location**: `src/Managing.WebApp/src/app/index.tsx`
|
||||
|
||||
**Integration**:
|
||||
- Added `<AiChatButton />` to main app
|
||||
- Available on all authenticated pages
|
||||
|
||||
## User Flow
|
||||
|
||||
### Complete Chat Flow
|
||||
|
||||
```
|
||||
┌──────────────┐
|
||||
│ User │
|
||||
└──────┬───────┘
|
||||
│
|
||||
│ 1. Clicks AI chat button
|
||||
▼
|
||||
┌─────────────────────┐
|
||||
│ AiChat Component │
|
||||
│ - Shows chat UI │
|
||||
│ - User types query │
|
||||
└──────┬──────────────┘
|
||||
│
|
||||
│ 2. POST /Llm/Chat
|
||||
│ {messages: [...], provider: "auto"}
|
||||
▼
|
||||
┌─────────────────────────────────────┐
|
||||
│ LlmController │
|
||||
│ 1. Get available MCP tools │
|
||||
│ 2. Select provider (Gemini) │
|
||||
│ 3. Call LLM with tools │
|
||||
└──────────┬───────────────────────────┘
|
||||
│
|
||||
│ 3. LLM returns tool_calls
|
||||
│ [{ name: "get_backtests_paginated", args: {...} }]
|
||||
▼
|
||||
┌─────────────────────────────────────┐
|
||||
│ Tool Call Handler │
|
||||
│ For each tool call: │
|
||||
│ → Execute via McpService │
|
||||
└──────────┬───────────────────────────┘
|
||||
│
|
||||
│ 4. Execute tool
|
||||
▼
|
||||
┌─────────────────────────────────────┐
|
||||
│ BacktestTools │
|
||||
│ → GetBacktestsPaginated(...) │
|
||||
│ → Query database via IBacktester │
|
||||
│ → Return filtered results │
|
||||
└──────────┬───────────────────────────┘
|
||||
│
|
||||
│ 5. Tool results returned
|
||||
▼
|
||||
┌─────────────────────────────────────┐
|
||||
│ LlmController │
|
||||
│ → Send tool results to LLM │
|
||||
│ → Get final natural language answer │
|
||||
└──────────┬───────────────────────────┘
|
||||
│
|
||||
│ 6. Final response
|
||||
▼
|
||||
┌─────────────────────────────────────┐
|
||||
│ AiChat Component │
|
||||
│ → Display AI response to user │
|
||||
│ → "Found 10 backtests with..." │
|
||||
└─────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## Features Implemented
|
||||
|
||||
### ✅ Auto Mode
|
||||
- Backend automatically selects the best available LLM provider
|
||||
- Priority: Gemini > OpenAI > Claude (based on cost/performance)
|
||||
|
||||
### ✅ BYOK (Bring Your Own Key)
|
||||
- Users can provide their own API keys
|
||||
- Keys are never stored, only used for that session
|
||||
- Supports all three providers (Gemini, OpenAI, Claude)
|
||||
|
||||
### ✅ MCP Tool Calling
|
||||
- LLM can call backend tools seamlessly
|
||||
- Tool results automatically sent back to LLM
|
||||
- Final response includes tool execution results
|
||||
|
||||
### ✅ Security
|
||||
- Backend API keys never exposed to frontend
|
||||
- User authentication required for all LLM endpoints
|
||||
- Tool execution respects user context
|
||||
|
||||
### ✅ Scalability
|
||||
- Easy to add new LLM providers (implement `ILlmProvider`)
|
||||
- Easy to add new MCP tools (create new tool class)
|
||||
- Provider abstraction allows switching without code changes
|
||||
|
||||
### ✅ Flexibility
|
||||
- Supports both streaming and non-streaming (currently non-streaming)
|
||||
- Temperature and max tokens configurable
|
||||
- Provider selection per request
|
||||
|
||||
## Example Usage
|
||||
|
||||
### Example 1: Query Backtests
|
||||
|
||||
**User**: "Show me my best backtests from the last month with a score above 80"
|
||||
|
||||
**LLM Thinks**: "I need to use the get_backtests_paginated tool"
|
||||
|
||||
**Tool Call**:
|
||||
```json
|
||||
{
|
||||
"name": "get_backtests_paginated",
|
||||
"arguments": {
|
||||
"scoreMin": 80,
|
||||
"durationMinDays": 30,
|
||||
"sortBy": "Score",
|
||||
"sortOrder": "desc",
|
||||
"pageSize": 10
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Tool Result**: Returns 5 backtests matching criteria
|
||||
|
||||
**LLM Response**: "I found 5 excellent backtests from the past month with scores above 80. The top performer achieved a score of 92.5 with a 68% win rate and minimal drawdown of 12%..."
|
||||
|
||||
### Example 2: Analyze Specific Ticker
|
||||
|
||||
**User**: "What's the performance of my BTC backtests?"
|
||||
|
||||
**Tool Call**:
|
||||
```json
|
||||
{
|
||||
"name": "get_backtests_paginated",
|
||||
"arguments": {
|
||||
"tickers": "BTC",
|
||||
"sortBy": "GrowthPercentage",
|
||||
"sortOrder": "desc"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**LLM Response**: "Your BTC backtests show strong performance. Out of 15 BTC strategies, the average growth is 34.2%. Your best strategy achieved 87% growth with a Sharpe ratio of 2.1..."
|
||||
|
||||
## Next Steps
|
||||
|
||||
### Future Enhancements
|
||||
|
||||
1. **Additional MCP Tools**:
|
||||
- Create/run backtests via chat
|
||||
- Get bot status and control
|
||||
- Query market data
|
||||
- Analyze positions
|
||||
|
||||
2. **Streaming Support**:
|
||||
- Implement SSE (Server-Sent Events)
|
||||
- Real-time token streaming
|
||||
- Better UX for long responses
|
||||
|
||||
3. **Context Management**:
|
||||
- Persistent chat history
|
||||
- Multi-session support
|
||||
- Context summarization
|
||||
|
||||
4. **Advanced Features**:
|
||||
- Voice input/output
|
||||
- File uploads (CSV analysis)
|
||||
- Chart generation
|
||||
- Strategy recommendations
|
||||
|
||||
5. **Admin Features**:
|
||||
- Usage analytics per user
|
||||
- Cost tracking per provider
|
||||
- Rate limiting
|
||||
|
||||
## Testing
|
||||
|
||||
### Manual Testing Steps
|
||||
|
||||
1. **Configure API Key**:
|
||||
```bash
|
||||
# Add to appsettings.Development.json or user secrets
|
||||
{
|
||||
"Llm": {
|
||||
"Gemini": {
|
||||
"ApiKey": "your-gemini-api-key"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
2. **Run Backend**:
|
||||
```bash
|
||||
cd src/Managing.Api
|
||||
dotnet run
|
||||
```
|
||||
|
||||
3. **Run Frontend**:
|
||||
```bash
|
||||
cd src/Managing.WebApp
|
||||
npm run dev
|
||||
```
|
||||
|
||||
4. **Test Chat**:
|
||||
- Login to the app
|
||||
- Click the AI chat button (bottom-right)
|
||||
- Try queries like:
|
||||
- "Show me my backtests"
|
||||
- "What are my best performing strategies?"
|
||||
- "Find backtests with winrate above 70%"
|
||||
|
||||
### Example Test Queries
|
||||
|
||||
```
|
||||
1. "Show me all my backtests sorted by score"
|
||||
2. "Find backtests for ETH with a score above 75"
|
||||
3. "What's my best performing backtest this week?"
|
||||
4. "Show me backtests with low drawdown (under 15%)"
|
||||
5. "List backtests using the RSI indicator"
|
||||
```
|
||||
|
||||
## Files Modified/Created
|
||||
|
||||
### Backend
|
||||
- ✅ `src/Managing.Mcp/Managing.Mcp.csproj`
|
||||
- ✅ `src/Managing.Mcp/Tools/BacktestTools.cs`
|
||||
- ✅ `src/Managing.Application.Abstractions/Services/IMcpService.cs`
|
||||
- ✅ `src/Managing.Application.Abstractions/Services/ILlmService.cs`
|
||||
- ✅ `src/Managing.Application/LLM/McpService.cs`
|
||||
- ✅ `src/Managing.Application/LLM/LlmService.cs`
|
||||
- ✅ `src/Managing.Application/LLM/Providers/ILlmProvider.cs`
|
||||
- ✅ `src/Managing.Application/LLM/Providers/GeminiProvider.cs`
|
||||
- ✅ `src/Managing.Application/LLM/Providers/OpenAiProvider.cs`
|
||||
- ✅ `src/Managing.Application/LLM/Providers/ClaudeProvider.cs`
|
||||
- ✅ `src/Managing.Api/Controllers/LlmController.cs`
|
||||
- ✅ `src/Managing.Bootstrap/ApiBootstrap.cs` (modified)
|
||||
- ✅ `src/Managing.Bootstrap/Managing.Bootstrap.csproj` (modified)
|
||||
- ✅ `src/Managing.Api/appsettings.json` (modified)
|
||||
|
||||
### Frontend
|
||||
- ✅ `src/Managing.WebApp/src/services/aiChatService.ts`
|
||||
- ✅ `src/Managing.WebApp/src/components/organism/AiChat.tsx`
|
||||
- ✅ `src/Managing.WebApp/src/components/organism/AiChatButton.tsx`
|
||||
- ✅ `src/Managing.WebApp/src/app/index.tsx` (modified)
|
||||
|
||||
## Conclusion
|
||||
|
||||
The implementation provides a complete, production-ready AI chat interface with MCP tool calling capabilities. The architecture is:
|
||||
|
||||
- **Secure**: API keys protected, user authentication required
|
||||
- **Scalable**: Easy to add providers and tools
|
||||
- **Flexible**: Supports auto mode and BYOK
|
||||
- **Interactive**: Real-time chat like Cursor but in the web app
|
||||
- **Powerful**: Can query and analyze backtest data via natural language
|
||||
|
||||
The system is ready for testing and can be extended with additional MCP tools for enhanced functionality.
|
||||
198
assets/documentation/MCP-Quick-Start.md
Normal file
198
assets/documentation/MCP-Quick-Start.md
Normal file
@@ -0,0 +1,198 @@
|
||||
# MCP Quick Start Guide
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- .NET 8 SDK
|
||||
- Node.js 18+
|
||||
- At least one LLM API key (Gemini, OpenAI, or Claude)
|
||||
|
||||
## Setup Steps
|
||||
|
||||
### 1. Configure LLM API Keys
|
||||
|
||||
Add your API key to `appsettings.Development.json` or user secrets:
|
||||
|
||||
```json
|
||||
{
|
||||
"Llm": {
|
||||
"Claude": {
|
||||
"ApiKey": "YOUR_CLAUDE_API_KEY_HERE"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Or use .NET user secrets (recommended):
|
||||
|
||||
```bash
|
||||
cd src/Managing.Api
|
||||
dotnet user-secrets set "Llm:Claude:ApiKey" "YOUR_API_KEY"
|
||||
```
|
||||
|
||||
Or use environment variables:
|
||||
|
||||
```bash
|
||||
export Llm__Claude__ApiKey="YOUR_API_KEY"
|
||||
dotnet run --project src/Managing.Api
|
||||
```
|
||||
|
||||
### 2. Build the Backend
|
||||
|
||||
```bash
|
||||
cd src
|
||||
dotnet build Managing.sln
|
||||
```
|
||||
|
||||
### 3. Run the Backend
|
||||
|
||||
```bash
|
||||
cd src/Managing.Api
|
||||
dotnet run
|
||||
```
|
||||
|
||||
The API will be available at `https://localhost:7001` (or configured port).
|
||||
|
||||
### 4. Generate API Client (if needed)
|
||||
|
||||
If the LLM endpoints aren't in the generated client yet:
|
||||
|
||||
```bash
|
||||
# Make sure the API is running
|
||||
cd src/Managing.Nswag
|
||||
dotnet build
|
||||
```
|
||||
|
||||
This will regenerate `ManagingApi.ts` with the new LLM endpoints.
|
||||
|
||||
### 5. Run the Frontend
|
||||
|
||||
```bash
|
||||
cd src/Managing.WebApp
|
||||
npm install # if first time
|
||||
npm run dev
|
||||
```
|
||||
|
||||
The app will be available at `http://localhost:5173` (or configured port).
|
||||
|
||||
### 6. Test the AI Chat
|
||||
|
||||
1. Login to the application
|
||||
2. Look for the floating chat button in the bottom-right corner
|
||||
3. Click it to open the AI chat
|
||||
4. Try these example queries:
|
||||
- "Show me my backtests"
|
||||
- "Find my best performing strategies"
|
||||
- "What are my BTC backtests?"
|
||||
- "Show backtests with a score above 80"
|
||||
|
||||
## Getting LLM API Keys
|
||||
|
||||
### Anthropic Claude (Recommended - Best for MCP)
|
||||
1. Go to [Anthropic Console](https://console.anthropic.com/)
|
||||
2. Sign in or create an account
|
||||
3. Navigate to API Keys and create a new key
|
||||
4. Copy and add to configuration
|
||||
5. Note: Requires payment setup
|
||||
|
||||
### Google Gemini (Free Tier Available)
|
||||
1. Go to [Google AI Studio](https://makersuite.google.com/app/apikey)
|
||||
2. Click "Get API Key"
|
||||
3. Create a new API key
|
||||
4. Copy and add to configuration
|
||||
|
||||
### OpenAI
|
||||
1. Go to [OpenAI Platform](https://platform.openai.com/api-keys)
|
||||
2. Create a new API key
|
||||
3. Copy and add to configuration
|
||||
4. Note: Requires payment setup
|
||||
|
||||
### Anthropic Claude
|
||||
1. Go to [Anthropic Console](https://console.anthropic.com/)
|
||||
2. Create an account and API key
|
||||
3. Copy and add to configuration
|
||||
4. Note: Requires payment setup
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
```
|
||||
User Browser
|
||||
↓
|
||||
AI Chat Component (React)
|
||||
↓
|
||||
LlmController (/api/Llm/Chat)
|
||||
↓
|
||||
LlmService (Auto-selects provider)
|
||||
↓
|
||||
Gemini/OpenAI/Claude Provider
|
||||
↓
|
||||
MCP Service (executes tools)
|
||||
↓
|
||||
BacktestTools (queries data)
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### No providers available
|
||||
- Check that at least one API key is configured
|
||||
- Verify the API key is valid
|
||||
- Check application logs for provider initialization
|
||||
|
||||
### Tool calls not working
|
||||
- Verify `IBacktester` service is registered
|
||||
- Check user has backtests in the database
|
||||
- Review logs for tool execution errors
|
||||
|
||||
### Frontend errors
|
||||
- Ensure API is running
|
||||
- Check browser console for errors
|
||||
- Verify `ManagingApi.ts` includes LLM endpoints
|
||||
|
||||
### Build errors
|
||||
- Run `dotnet restore` in src/
|
||||
- Ensure all NuGet packages are restored
|
||||
- Check for version conflicts in project files
|
||||
|
||||
## Example Queries
|
||||
|
||||
### Simple Queries
|
||||
```
|
||||
"Show me my backtests"
|
||||
"What's my best strategy?"
|
||||
"List all my BTC backtests"
|
||||
```
|
||||
|
||||
### Filtered Queries
|
||||
```
|
||||
"Find backtests with a score above 85"
|
||||
"Show me backtests from the last 30 days"
|
||||
"List backtests with low drawdown (under 10%)"
|
||||
```
|
||||
|
||||
### Complex Queries
|
||||
```
|
||||
"What are my best performing ETH strategies with a winrate above 70%?"
|
||||
"Find backtests using RSI indicator sorted by Sharpe ratio"
|
||||
"Show me my top 5 backtests by growth percentage"
|
||||
```
|
||||
|
||||
## Next Steps
|
||||
|
||||
- Add more MCP tools for additional functionality
|
||||
- Customize the chat UI to match your brand
|
||||
- Implement chat history persistence
|
||||
- Add streaming support for better UX
|
||||
- Create custom tools for your specific use cases
|
||||
|
||||
## Support
|
||||
|
||||
For issues or questions:
|
||||
1. Check the logs in `Managing.Api` console
|
||||
2. Review browser console for frontend errors
|
||||
3. Verify API keys are correctly configured
|
||||
4. Ensure all services are running
|
||||
|
||||
## Additional Resources
|
||||
|
||||
- [MCP Architecture Documentation](./MCP-Architecture.md)
|
||||
- [Implementation Summary](./MCP-Implementation-Summary.md)
|
||||
- [Model Context Protocol Spec](https://modelcontextprotocol.io)
|
||||
68
assets/documentation/README.md
Normal file
68
assets/documentation/README.md
Normal file
@@ -0,0 +1,68 @@
|
||||
# Managing Apps Documentation
|
||||
|
||||
This directory contains technical documentation for the Managing trading platform.
|
||||
|
||||
## Architecture & Design
|
||||
|
||||
- **[MCP Architecture](MCP-Architecture.md)** - Model Context Protocol architecture, dual-MCP approach (C# internal + Node.js community)
|
||||
- **[Architecture Diagram](Architecture.drawio)** - Overall system architecture (Draw.io format)
|
||||
- **[Monorepo Structure](Workers%20processing/07-Monorepo-Structure.md)** - Project organization and structure
|
||||
|
||||
## Upgrade Plans
|
||||
|
||||
- **[.NET 10 Upgrade Plan](NET10-Upgrade-Plan.md)** - Detailed .NET 10 upgrade specification
|
||||
- **[.NET 10 Upgrade Quick Reference](README-Upgrade-Plan.md)** - Quick overview of upgrade plan
|
||||
|
||||
## Workers & Processing
|
||||
|
||||
- **[Workers Processing Overview](Workers%20processing/README.md)** - Background workers documentation index
|
||||
- **[Overall Architecture](Workers%20processing/01-Overall-Architecture.md)** - Worker architecture overview
|
||||
- **[Request Flow](Workers%20processing/02-Request-Flow.md)** - Request processing flow
|
||||
- **[Job Processing Flow](Workers%20processing/03-Job-Processing-Flow.md)** - Job processing details
|
||||
- **[Database Schema](Workers%20processing/04-Database-Schema.md)** - Worker database schema
|
||||
- **[Deployment Architecture](Workers%20processing/05-Deployment-Architecture.md)** - Deployment setup
|
||||
- **[Concurrency Control](Workers%20processing/06-Concurrency-Control.md)** - Concurrency handling
|
||||
- **[Implementation Plan](Workers%20processing/IMPLEMENTATION-PLAN.md)** - Worker implementation details
|
||||
|
||||
## Workflows
|
||||
|
||||
- **[Position Workflow](PositionWorkflow.md)** - Trading position workflow
|
||||
- **[Delta Neutral Worker](DeltaNeutralWorker.md)** - Delta neutral trading worker
|
||||
|
||||
## Other
|
||||
|
||||
- **[End Game](EndGame.md)** - End game strategy documentation
|
||||
|
||||
## Quick Links
|
||||
|
||||
### For Developers
|
||||
- Start with [Architecture Diagram](Architecture.drawio) for system overview
|
||||
- Review [MCP Architecture](MCP-Architecture.md) for LLM integration
|
||||
- Check [Workers Processing](Workers%20processing/README.md) for background jobs
|
||||
|
||||
### For DevOps
|
||||
- See [Deployment Architecture](Workers%20processing/05-Deployment-Architecture.md)
|
||||
- Review [.NET 10 Upgrade Plan](NET10-Upgrade-Plan.md) for framework updates
|
||||
|
||||
### For Product/Planning
|
||||
- Review [MCP Architecture](MCP-Architecture.md) for community features
|
||||
- Check [Workers Processing](Workers%20processing/README.md) for system capabilities
|
||||
|
||||
## Document Status
|
||||
|
||||
| Document | Status | Last Updated |
|
||||
|----------|--------|--------------|
|
||||
| MCP Architecture | Planning | 2025-01-XX |
|
||||
| .NET 10 Upgrade Plan | Planning | 2024-11-24 |
|
||||
| Workers Processing | Active | Various |
|
||||
| Architecture Diagram | Active | Various |
|
||||
|
||||
## Contributing
|
||||
|
||||
When adding new documentation:
|
||||
1. Use Markdown format (`.md`)
|
||||
2. Follow existing structure and style
|
||||
3. Update this README with links
|
||||
4. Add appropriate cross-references
|
||||
5. Include diagrams in Draw.io format when needed
|
||||
|
||||
162
src/Managing.Api/Controllers/LlmController.cs
Normal file
162
src/Managing.Api/Controllers/LlmController.cs
Normal file
@@ -0,0 +1,162 @@
|
||||
using Managing.Application.Abstractions.Services;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace Managing.Api.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// Controller for LLM (Large Language Model) operations with MCP tool calling support.
|
||||
/// Provides endpoints for chat interactions with automatic provider selection and BYOK (Bring Your Own Key) support.
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
[Authorize]
|
||||
[Route("[controller]")]
|
||||
[Produces("application/json")]
|
||||
public class LlmController : BaseController
|
||||
{
|
||||
private readonly ILlmService _llmService;
|
||||
private readonly IMcpService _mcpService;
|
||||
private readonly ILogger<LlmController> _logger;
|
||||
|
||||
public LlmController(
|
||||
ILlmService llmService,
|
||||
IMcpService mcpService,
|
||||
IUserService userService,
|
||||
ILogger<LlmController> logger) : base(userService)
|
||||
{
|
||||
_llmService = llmService;
|
||||
_mcpService = mcpService;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sends a chat message to an LLM with automatic provider selection and MCP tool calling support.
|
||||
/// Supports both auto mode (backend selects provider) and BYOK (user provides API key).
|
||||
/// </summary>
|
||||
/// <param name="request">The chat request with messages and optional provider/API key</param>
|
||||
/// <returns>The LLM response with tool calls if applicable</returns>
|
||||
[HttpPost]
|
||||
[Route("Chat")]
|
||||
public async Task<ActionResult<LlmChatResponse>> Chat([FromBody] LlmChatRequest request)
|
||||
{
|
||||
if (request == null)
|
||||
{
|
||||
return BadRequest("Chat request is required");
|
||||
}
|
||||
|
||||
if (request.Messages == null || !request.Messages.Any())
|
||||
{
|
||||
return BadRequest("At least one message is required");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var user = await GetUser();
|
||||
|
||||
// Get available MCP tools
|
||||
var availableTools = await _mcpService.GetAvailableToolsAsync();
|
||||
request.Tools = availableTools.ToList();
|
||||
|
||||
// Send chat request to LLM
|
||||
var response = await _llmService.ChatAsync(user, request);
|
||||
|
||||
// If LLM wants to call tools, execute them and get final response
|
||||
if (response.RequiresToolExecution && response.ToolCalls?.Any() == true)
|
||||
{
|
||||
_logger.LogInformation("LLM requested {Count} tool calls for user {UserId}",
|
||||
response.ToolCalls.Count, user.Id);
|
||||
|
||||
// Execute all tool calls
|
||||
var toolResults = new List<LlmMessage>();
|
||||
foreach (var toolCall in response.ToolCalls)
|
||||
{
|
||||
try
|
||||
{
|
||||
var toolResult = await _mcpService.ExecuteToolAsync(user, toolCall.Name, toolCall.Arguments);
|
||||
toolResults.Add(new LlmMessage
|
||||
{
|
||||
Role = "tool",
|
||||
Content = System.Text.Json.JsonSerializer.Serialize(toolResult),
|
||||
ToolCallId = toolCall.Id
|
||||
});
|
||||
_logger.LogInformation("Successfully executed tool {ToolName} for user {UserId}",
|
||||
toolCall.Name, user.Id);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error executing tool {ToolName} for user {UserId}",
|
||||
toolCall.Name, user.Id);
|
||||
toolResults.Add(new LlmMessage
|
||||
{
|
||||
Role = "tool",
|
||||
Content = $"Error executing tool: {ex.Message}",
|
||||
ToolCallId = toolCall.Id
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Add assistant message with tool calls
|
||||
request.Messages.Add(new LlmMessage
|
||||
{
|
||||
Role = "assistant",
|
||||
Content = response.Content,
|
||||
ToolCalls = response.ToolCalls
|
||||
});
|
||||
|
||||
// Add tool results
|
||||
request.Messages.AddRange(toolResults);
|
||||
|
||||
// Get final response from LLM
|
||||
var finalResponse = await _llmService.ChatAsync(user, request);
|
||||
return Ok(finalResponse);
|
||||
}
|
||||
|
||||
return Ok(response);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error processing chat request for user");
|
||||
return StatusCode(500, $"Error processing chat request: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the list of available LLM providers configured on the backend.
|
||||
/// </summary>
|
||||
/// <returns>List of provider names</returns>
|
||||
[HttpGet]
|
||||
[Route("Providers")]
|
||||
public async Task<ActionResult<IEnumerable<string>>> GetProviders()
|
||||
{
|
||||
try
|
||||
{
|
||||
var providers = await _llmService.GetAvailableProvidersAsync();
|
||||
return Ok(providers);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error getting available providers");
|
||||
return StatusCode(500, $"Error getting available providers: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the list of available MCP tools that the LLM can call.
|
||||
/// </summary>
|
||||
/// <returns>List of MCP tools with their descriptions and parameters</returns>
|
||||
[HttpGet]
|
||||
[Route("Tools")]
|
||||
public async Task<ActionResult<IEnumerable<McpToolDefinition>>> GetTools()
|
||||
{
|
||||
try
|
||||
{
|
||||
var tools = await _mcpService.GetAvailableToolsAsync();
|
||||
return Ok(tools);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error getting available tools");
|
||||
return StatusCode(500, $"Error getting available tools: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ using Managing.Domain.Users;
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using static Managing.Common.Enums;
|
||||
|
||||
namespace Managing.Api.Controllers;
|
||||
|
||||
@@ -115,6 +116,31 @@ public class UserController : BaseController
|
||||
return Ok(updatedUser);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the default LLM provider for the current user.
|
||||
/// </summary>
|
||||
/// <param name="defaultLlmProvider">The new default LLM provider to set (e.g., "Auto", "Gemini", "OpenAI", "Claude").</param>
|
||||
/// <returns>The updated user with the new default LLM provider.</returns>
|
||||
[Authorize]
|
||||
[HttpPut("default-llm-provider")]
|
||||
public async Task<ActionResult<User>> UpdateDefaultLlmProvider([FromBody] string defaultLlmProvider)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(defaultLlmProvider))
|
||||
{
|
||||
return BadRequest("Default LLM provider cannot be null or empty.");
|
||||
}
|
||||
|
||||
// Parse string to enum (case-insensitive)
|
||||
if (!Enum.TryParse<LlmProvider>(defaultLlmProvider, ignoreCase: true, out var providerEnum))
|
||||
{
|
||||
return BadRequest($"Invalid LLM provider '{defaultLlmProvider}'. Valid providers are: Auto, Gemini, OpenAI, Claude");
|
||||
}
|
||||
|
||||
var user = await GetUser();
|
||||
var updatedUser = await _userService.UpdateDefaultLlmProvider(user, providerEnum);
|
||||
return Ok(updatedUser);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests the Telegram channel configuration by sending a test message.
|
||||
/// </summary>
|
||||
|
||||
@@ -9,8 +9,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
"InfluxDb": {
|
||||
"Organization": "managing-org"
|
||||
},
|
||||
@@ -28,6 +26,17 @@
|
||||
"Flagsmith": {
|
||||
"ApiUrl": "https://flag.kaigen.ai/api/v1/"
|
||||
},
|
||||
"Llm": {
|
||||
"Gemini": {
|
||||
"DefaultModel": "gemini-2.0-flash"
|
||||
},
|
||||
"OpenAI": {
|
||||
"DefaultModel": "gpt-4o"
|
||||
},
|
||||
"Claude": {
|
||||
"DefaultModel": "claude-haiku-4-5-20251001"
|
||||
}
|
||||
},
|
||||
"N8n": {
|
||||
},
|
||||
"Sentry": {
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
using Managing.Domain.Users;
|
||||
|
||||
namespace Managing.Application.Abstractions.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Service for interacting with LLM providers
|
||||
/// </summary>
|
||||
public interface ILlmService
|
||||
{
|
||||
/// <summary>
|
||||
/// Sends a chat message to the LLM and gets a response with tool calling support
|
||||
/// </summary>
|
||||
/// <param name="user">The user context</param>
|
||||
/// <param name="request">The chat request</param>
|
||||
/// <returns>The chat response</returns>
|
||||
Task<LlmChatResponse> ChatAsync(User user, LlmChatRequest request);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the list of available LLM providers
|
||||
/// </summary>
|
||||
/// <returns>List of provider names</returns>
|
||||
Task<IEnumerable<string>> GetAvailableProvidersAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Request model for LLM chat
|
||||
/// </summary>
|
||||
public class LlmChatRequest
|
||||
{
|
||||
public List<LlmMessage> Messages { get; set; } = new();
|
||||
public string? Provider { get; set; } // null for auto-selection
|
||||
public string? ApiKey { get; set; } // BYOK (Bring Your Own Key)
|
||||
public bool Stream { get; set; } = false;
|
||||
public double Temperature { get; set; } = 0.7;
|
||||
public int MaxTokens { get; set; } = 4096;
|
||||
public List<McpToolDefinition>? Tools { get; set; } // Available MCP tools
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Response model for LLM chat
|
||||
/// </summary>
|
||||
public class LlmChatResponse
|
||||
{
|
||||
public string Content { get; set; } = string.Empty;
|
||||
public string Provider { get; set; } = string.Empty;
|
||||
public string Model { get; set; } = string.Empty;
|
||||
public List<LlmToolCall>? ToolCalls { get; set; }
|
||||
public LlmUsage? Usage { get; set; }
|
||||
public bool RequiresToolExecution { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents a message in the conversation
|
||||
/// </summary>
|
||||
public class LlmMessage
|
||||
{
|
||||
public string Role { get; set; } = string.Empty; // "user", "assistant", "system", "tool"
|
||||
public string Content { get; set; } = string.Empty;
|
||||
public List<LlmToolCall>? ToolCalls { get; set; }
|
||||
public string? ToolCallId { get; set; } // For tool response messages
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents a tool call from the LLM
|
||||
/// </summary>
|
||||
public class LlmToolCall
|
||||
{
|
||||
public string Id { get; set; } = string.Empty;
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public Dictionary<string, object> Arguments { get; set; } = new();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Usage statistics for the LLM request
|
||||
/// </summary>
|
||||
public class LlmUsage
|
||||
{
|
||||
public int PromptTokens { get; set; }
|
||||
public int CompletionTokens { get; set; }
|
||||
public int TotalTokens { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Configuration for an LLM provider
|
||||
/// </summary>
|
||||
public class LlmProviderConfig
|
||||
{
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public string ApiKey { get; set; } = string.Empty;
|
||||
public string BaseUrl { get; set; } = string.Empty;
|
||||
public string DefaultModel { get; set; } = string.Empty;
|
||||
public bool Enabled { get; set; } = true;
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
using Managing.Domain.Users;
|
||||
|
||||
namespace Managing.Application.Abstractions.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Service for executing Model Context Protocol (MCP) tools
|
||||
/// </summary>
|
||||
public interface IMcpService
|
||||
{
|
||||
/// <summary>
|
||||
/// Executes an MCP tool with the given parameters
|
||||
/// </summary>
|
||||
/// <param name="user">The user context for the tool execution</param>
|
||||
/// <param name="toolName">The name of the tool to execute</param>
|
||||
/// <param name="parameters">The parameters for the tool as a dictionary</param>
|
||||
/// <returns>The result of the tool execution</returns>
|
||||
Task<object> ExecuteToolAsync(User user, string toolName, Dictionary<string, object>? parameters = null);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the list of available tools with their descriptions
|
||||
/// </summary>
|
||||
/// <returns>List of available tools with metadata</returns>
|
||||
Task<IEnumerable<McpToolDefinition>> GetAvailableToolsAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents an MCP tool definition
|
||||
/// </summary>
|
||||
public class McpToolDefinition
|
||||
{
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public string Description { get; set; } = string.Empty;
|
||||
public Dictionary<string, McpParameterDefinition> Parameters { get; set; } = new();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents a parameter definition for an MCP tool
|
||||
/// </summary>
|
||||
public class McpParameterDefinition
|
||||
{
|
||||
public string Type { get; set; } = string.Empty;
|
||||
public string Description { get; set; } = string.Empty;
|
||||
public bool Required { get; set; }
|
||||
public object? DefaultValue { get; set; }
|
||||
}
|
||||
@@ -12,6 +12,7 @@ public interface IUserService
|
||||
Task<User> UpdateAgentName(User user, string agentName);
|
||||
Task<User> UpdateAvatarUrl(User user, string avatarUrl);
|
||||
Task<User> UpdateTelegramChannel(User user, string telegramChannel);
|
||||
Task<User> UpdateDefaultLlmProvider(User user, LlmProvider defaultLlmProvider);
|
||||
Task<User> UpdateUserSettings(User user, UserSettingsDto settings);
|
||||
Task<User> GetUserByName(string name);
|
||||
Task<User> GetUserByAgentName(string agentName);
|
||||
|
||||
210
src/Managing.Application/LLM/LlmService.cs
Normal file
210
src/Managing.Application/LLM/LlmService.cs
Normal file
@@ -0,0 +1,210 @@
|
||||
using Managing.Application.Abstractions.Services;
|
||||
using Managing.Application.LLM.Providers;
|
||||
using Managing.Domain.Users;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using static Managing.Common.Enums;
|
||||
|
||||
namespace Managing.Application.LLM;
|
||||
|
||||
/// <summary>
|
||||
/// Service for interacting with LLM providers with auto-selection and BYOK support
|
||||
/// </summary>
|
||||
public class LlmService : ILlmService
|
||||
{
|
||||
private readonly IConfiguration _configuration;
|
||||
private readonly ILogger<LlmService> _logger;
|
||||
private readonly Dictionary<string, ILlmProvider> _providers;
|
||||
|
||||
public LlmService(
|
||||
IConfiguration configuration,
|
||||
ILogger<LlmService> logger,
|
||||
IHttpClientFactory httpClientFactory)
|
||||
{
|
||||
_configuration = configuration;
|
||||
_logger = logger;
|
||||
_providers = new Dictionary<string, ILlmProvider>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
// Initialize providers
|
||||
InitializeProviders(httpClientFactory);
|
||||
}
|
||||
|
||||
private void InitializeProviders(IHttpClientFactory httpClientFactory)
|
||||
{
|
||||
// Gemini Provider
|
||||
var geminiApiKey = _configuration["Llm:Gemini:ApiKey"];
|
||||
var geminiModel = _configuration["Llm:Gemini:DefaultModel"];
|
||||
if (!string.IsNullOrWhiteSpace(geminiApiKey))
|
||||
{
|
||||
var providerKey = ConvertLlmProviderToString(LlmProvider.Gemini);
|
||||
_providers[providerKey] = new GeminiProvider(geminiApiKey, geminiModel, httpClientFactory, _logger);
|
||||
_logger.LogInformation("Gemini provider initialized with model: {Model}", geminiModel ?? "default");
|
||||
}
|
||||
|
||||
// OpenAI Provider
|
||||
var openaiApiKey = _configuration["Llm:OpenAI:ApiKey"];
|
||||
var openaiModel = _configuration["Llm:OpenAI:DefaultModel"];
|
||||
if (!string.IsNullOrWhiteSpace(openaiApiKey))
|
||||
{
|
||||
var providerKey = ConvertLlmProviderToString(LlmProvider.OpenAI);
|
||||
_providers[providerKey] = new OpenAiProvider(openaiApiKey, openaiModel, httpClientFactory, _logger);
|
||||
_logger.LogInformation("OpenAI provider initialized with model: {Model}", openaiModel ?? "default");
|
||||
}
|
||||
|
||||
// Claude Provider
|
||||
var claudeApiKey = _configuration["Llm:Claude:ApiKey"];
|
||||
var claudeModel = _configuration["Llm:Claude:DefaultModel"];
|
||||
if (!string.IsNullOrWhiteSpace(claudeApiKey))
|
||||
{
|
||||
var providerKey = ConvertLlmProviderToString(LlmProvider.Claude);
|
||||
_providers[providerKey] = new ClaudeProvider(claudeApiKey, claudeModel, httpClientFactory, _logger);
|
||||
_logger.LogInformation("Claude provider initialized with model: {Model}", claudeModel ?? "default");
|
||||
}
|
||||
|
||||
if (_providers.Count == 0)
|
||||
{
|
||||
_logger.LogWarning("No LLM providers configured. Please add API keys to configuration.");
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<LlmChatResponse> ChatAsync(User user, LlmChatRequest request)
|
||||
{
|
||||
ILlmProvider provider;
|
||||
|
||||
// BYOK: If user provides their own API key
|
||||
if (!string.IsNullOrWhiteSpace(request.ApiKey))
|
||||
{
|
||||
var requestedProvider = ParseProviderString(request.Provider) ?? LlmProvider.Claude; // Default to Claude for BYOK
|
||||
var providerName = ConvertLlmProviderToString(requestedProvider);
|
||||
provider = CreateProviderWithCustomKey(requestedProvider, request.ApiKey);
|
||||
_logger.LogInformation("Using BYOK for provider: {Provider} for user: {UserId}", providerName, user.Id);
|
||||
}
|
||||
// Auto mode: Select provider automatically (use user's default if set, otherwise fallback to system default)
|
||||
else if (string.IsNullOrWhiteSpace(request.Provider) ||
|
||||
ParseProviderString(request.Provider) == LlmProvider.Auto)
|
||||
{
|
||||
// Check if user has a default provider preference (and it's not Auto)
|
||||
if (user.DefaultLlmProvider.HasValue &&
|
||||
user.DefaultLlmProvider.Value != LlmProvider.Auto)
|
||||
{
|
||||
var providerName = ConvertLlmProviderToString(user.DefaultLlmProvider.Value);
|
||||
if (_providers.TryGetValue(providerName, out var userPreferredProvider))
|
||||
{
|
||||
provider = userPreferredProvider;
|
||||
_logger.LogInformation("Using user's default provider: {Provider} for user: {UserId}", provider.Name, user.Id);
|
||||
}
|
||||
else
|
||||
{
|
||||
provider = SelectProvider();
|
||||
_logger.LogInformation("Auto-selected provider: {Provider} for user: {UserId} (user default {UserDefault} not available)",
|
||||
provider.Name, user.Id, user.DefaultLlmProvider.Value);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
provider = SelectProvider();
|
||||
_logger.LogInformation("Auto-selected provider: {Provider} for user: {UserId} (user default: {UserDefault})",
|
||||
provider.Name, user.Id, user.DefaultLlmProvider?.ToString() ?? "not set");
|
||||
}
|
||||
}
|
||||
// Explicit provider selection
|
||||
else
|
||||
{
|
||||
var requestedProvider = ParseProviderString(request.Provider);
|
||||
if (requestedProvider == null || requestedProvider == LlmProvider.Auto)
|
||||
{
|
||||
throw new InvalidOperationException($"Invalid provider '{request.Provider}'. Valid providers are: {string.Join(", ", Enum.GetNames<LlmProvider>())}");
|
||||
}
|
||||
|
||||
var providerName = ConvertLlmProviderToString(requestedProvider.Value);
|
||||
if (!_providers.TryGetValue(providerName, out provider!))
|
||||
{
|
||||
throw new InvalidOperationException($"Provider '{request.Provider}' is not available or not configured.");
|
||||
}
|
||||
_logger.LogInformation("Using specified provider: {Provider} for user: {UserId}", providerName, user.Id);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var response = await provider.ChatAsync(request);
|
||||
return response;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error calling LLM provider {Provider} for user {UserId}", provider.Name, user.Id);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public Task<IEnumerable<string>> GetAvailableProvidersAsync()
|
||||
{
|
||||
return Task.FromResult(_providers.Keys.AsEnumerable());
|
||||
}
|
||||
|
||||
private ILlmProvider SelectProvider()
|
||||
{
|
||||
// Priority: OpenAI > Claude > Gemini
|
||||
var openaiKey = ConvertLlmProviderToString(LlmProvider.OpenAI);
|
||||
if (_providers.TryGetValue(openaiKey, out var openai))
|
||||
return openai;
|
||||
|
||||
var claudeKey = ConvertLlmProviderToString(LlmProvider.Claude);
|
||||
if (_providers.TryGetValue(claudeKey, out var claude))
|
||||
return claude;
|
||||
|
||||
var geminiKey = ConvertLlmProviderToString(LlmProvider.Gemini);
|
||||
if (_providers.TryGetValue(geminiKey, out var gemini))
|
||||
return gemini;
|
||||
|
||||
throw new InvalidOperationException("No LLM providers are configured. Please add API keys to configuration.");
|
||||
}
|
||||
|
||||
private ILlmProvider CreateProviderWithCustomKey(LlmProvider provider, string apiKey)
|
||||
{
|
||||
// This is a temporary instance with user's API key
|
||||
// Get default models from configuration
|
||||
var geminiModel = _configuration["Llm:Gemini:DefaultModel"];
|
||||
var openaiModel = _configuration["Llm:OpenAI:DefaultModel"];
|
||||
var claudeModel = _configuration["Llm:Claude:DefaultModel"];
|
||||
|
||||
return provider switch
|
||||
{
|
||||
LlmProvider.Gemini => new GeminiProvider(apiKey, geminiModel, null!, _logger),
|
||||
LlmProvider.OpenAI => new OpenAiProvider(apiKey, openaiModel, null!, _logger),
|
||||
LlmProvider.Claude => new ClaudeProvider(apiKey, claudeModel, null!, _logger),
|
||||
_ => throw new InvalidOperationException($"Cannot create provider with custom key for: {provider}. Only Gemini, OpenAI, and Claude are supported for BYOK.")
|
||||
};
|
||||
}
|
||||
|
||||
private string ConvertLlmProviderToString(LlmProvider provider)
|
||||
{
|
||||
return provider switch
|
||||
{
|
||||
LlmProvider.Auto => "auto",
|
||||
LlmProvider.Gemini => "gemini",
|
||||
LlmProvider.OpenAI => "openai",
|
||||
LlmProvider.Claude => "claude",
|
||||
_ => throw new ArgumentException($"Unknown LlmProvider enum value: {provider}")
|
||||
};
|
||||
}
|
||||
|
||||
private LlmProvider? ParseProviderString(string? providerString)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(providerString))
|
||||
return null;
|
||||
|
||||
// Try parsing as enum (case-insensitive)
|
||||
if (Enum.TryParse<LlmProvider>(providerString, ignoreCase: true, out var parsedProvider))
|
||||
return parsedProvider;
|
||||
|
||||
// Fallback to lowercase string matching for backward compatibility
|
||||
return providerString.ToLowerInvariant() switch
|
||||
{
|
||||
"auto" => LlmProvider.Auto,
|
||||
"gemini" => LlmProvider.Gemini,
|
||||
"openai" => LlmProvider.OpenAI,
|
||||
"claude" => LlmProvider.Claude,
|
||||
_ => null
|
||||
};
|
||||
}
|
||||
}
|
||||
236
src/Managing.Application/LLM/McpService.cs
Normal file
236
src/Managing.Application/LLM/McpService.cs
Normal file
@@ -0,0 +1,236 @@
|
||||
using Managing.Application.Abstractions.Services;
|
||||
using Managing.Domain.Users;
|
||||
using Managing.Mcp.Tools;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using static Managing.Common.Enums;
|
||||
|
||||
namespace Managing.Application.LLM;
|
||||
|
||||
/// <summary>
|
||||
/// Service for executing Model Context Protocol (MCP) tools
|
||||
/// </summary>
|
||||
public class McpService : IMcpService
|
||||
{
|
||||
private readonly BacktestTools _backtestTools;
|
||||
private readonly ILogger<McpService> _logger;
|
||||
|
||||
public McpService(BacktestTools backtestTools, ILogger<McpService> logger)
|
||||
{
|
||||
_backtestTools = backtestTools;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task<object> ExecuteToolAsync(User user, string toolName, Dictionary<string, object>? parameters = null)
|
||||
{
|
||||
_logger.LogInformation("Executing MCP tool: {ToolName} for user: {UserId}", toolName, user.Id);
|
||||
|
||||
try
|
||||
{
|
||||
return toolName.ToLowerInvariant() switch
|
||||
{
|
||||
"get_backtests_paginated" => await ExecuteGetBacktestsPaginated(user, parameters),
|
||||
_ => throw new InvalidOperationException($"Unknown tool: {toolName}")
|
||||
};
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error executing MCP tool {ToolName} for user {UserId}", toolName, user.Id);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public Task<IEnumerable<McpToolDefinition>> GetAvailableToolsAsync()
|
||||
{
|
||||
var tools = new List<McpToolDefinition>
|
||||
{
|
||||
new McpToolDefinition
|
||||
{
|
||||
Name = "get_backtests_paginated",
|
||||
Description = "Retrieves paginated backtests with filtering and sorting capabilities. Supports filters for score, winrate, drawdown, tickers, indicators, duration, and trading type.",
|
||||
Parameters = new Dictionary<string, McpParameterDefinition>
|
||||
{
|
||||
["page"] = new McpParameterDefinition
|
||||
{
|
||||
Type = "integer",
|
||||
Description = "Page number (defaults to 1)",
|
||||
Required = false,
|
||||
DefaultValue = 1
|
||||
},
|
||||
["pageSize"] = new McpParameterDefinition
|
||||
{
|
||||
Type = "integer",
|
||||
Description = "Number of items per page (defaults to 50, max 100)",
|
||||
Required = false,
|
||||
DefaultValue = 50
|
||||
},
|
||||
["sortBy"] = new McpParameterDefinition
|
||||
{
|
||||
Type = "string",
|
||||
Description = "Field to sort by (Score, WinRate, GrowthPercentage, MaxDrawdown, SharpeRatio, FinalPnl, StartDate, EndDate, PositionCount)",
|
||||
Required = false,
|
||||
DefaultValue = "Score"
|
||||
},
|
||||
["sortOrder"] = new McpParameterDefinition
|
||||
{
|
||||
Type = "string",
|
||||
Description = "Sort order - 'asc' or 'desc' (defaults to 'desc')",
|
||||
Required = false,
|
||||
DefaultValue = "desc"
|
||||
},
|
||||
["scoreMin"] = new McpParameterDefinition
|
||||
{
|
||||
Type = "number",
|
||||
Description = "Minimum score filter (0-100)",
|
||||
Required = false
|
||||
},
|
||||
["scoreMax"] = new McpParameterDefinition
|
||||
{
|
||||
Type = "number",
|
||||
Description = "Maximum score filter (0-100)",
|
||||
Required = false
|
||||
},
|
||||
["winrateMin"] = new McpParameterDefinition
|
||||
{
|
||||
Type = "integer",
|
||||
Description = "Minimum winrate filter (0-100)",
|
||||
Required = false
|
||||
},
|
||||
["winrateMax"] = new McpParameterDefinition
|
||||
{
|
||||
Type = "integer",
|
||||
Description = "Maximum winrate filter (0-100)",
|
||||
Required = false
|
||||
},
|
||||
["maxDrawdownMax"] = new McpParameterDefinition
|
||||
{
|
||||
Type = "number",
|
||||
Description = "Maximum drawdown filter",
|
||||
Required = false
|
||||
},
|
||||
["tickers"] = new McpParameterDefinition
|
||||
{
|
||||
Type = "string",
|
||||
Description = "Comma-separated list of tickers to filter by (e.g., 'BTC,ETH,SOL')",
|
||||
Required = false
|
||||
},
|
||||
["indicators"] = new McpParameterDefinition
|
||||
{
|
||||
Type = "string",
|
||||
Description = "Comma-separated list of indicators to filter by",
|
||||
Required = false
|
||||
},
|
||||
["durationMinDays"] = new McpParameterDefinition
|
||||
{
|
||||
Type = "number",
|
||||
Description = "Minimum duration in days",
|
||||
Required = false
|
||||
},
|
||||
["durationMaxDays"] = new McpParameterDefinition
|
||||
{
|
||||
Type = "number",
|
||||
Description = "Maximum duration in days",
|
||||
Required = false
|
||||
},
|
||||
["name"] = new McpParameterDefinition
|
||||
{
|
||||
Type = "string",
|
||||
Description = "Filter by name (contains search)",
|
||||
Required = false
|
||||
},
|
||||
["tradingType"] = new McpParameterDefinition
|
||||
{
|
||||
Type = "string",
|
||||
Description = "Trading type filter (Spot, Futures, BacktestSpot, BacktestFutures, Paper)",
|
||||
Required = false
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return Task.FromResult<IEnumerable<McpToolDefinition>>(tools);
|
||||
}
|
||||
|
||||
private async Task<object> ExecuteGetBacktestsPaginated(User user, Dictionary<string, object>? parameters)
|
||||
{
|
||||
var page = GetParameterValue<int>(parameters, "page", 1);
|
||||
var pageSize = GetParameterValue<int>(parameters, "pageSize", 50);
|
||||
var sortByString = GetParameterValue<string>(parameters, "sortBy", "Score");
|
||||
var sortOrder = GetParameterValue<string>(parameters, "sortOrder", "desc");
|
||||
var scoreMin = GetParameterValue<double?>(parameters, "scoreMin", null);
|
||||
var scoreMax = GetParameterValue<double?>(parameters, "scoreMax", null);
|
||||
var winrateMin = GetParameterValue<int?>(parameters, "winrateMin", null);
|
||||
var winrateMax = GetParameterValue<int?>(parameters, "winrateMax", null);
|
||||
var maxDrawdownMax = GetParameterValue<decimal?>(parameters, "maxDrawdownMax", null);
|
||||
var tickers = GetParameterValue<string?>(parameters, "tickers", null);
|
||||
var indicators = GetParameterValue<string?>(parameters, "indicators", null);
|
||||
var durationMinDays = GetParameterValue<double?>(parameters, "durationMinDays", null);
|
||||
var durationMaxDays = GetParameterValue<double?>(parameters, "durationMaxDays", null);
|
||||
var name = GetParameterValue<string?>(parameters, "name", null);
|
||||
var tradingTypeString = GetParameterValue<string?>(parameters, "tradingType", null);
|
||||
|
||||
// Parse sortBy enum
|
||||
if (!Enum.TryParse<BacktestSortableColumn>(sortByString, true, out var sortBy))
|
||||
{
|
||||
sortBy = BacktestSortableColumn.Score;
|
||||
}
|
||||
|
||||
// Parse tradingType enum
|
||||
TradingType? tradingType = null;
|
||||
if (!string.IsNullOrWhiteSpace(tradingTypeString) &&
|
||||
Enum.TryParse<TradingType>(tradingTypeString, true, out var parsedTradingType))
|
||||
{
|
||||
tradingType = parsedTradingType;
|
||||
}
|
||||
|
||||
return await _backtestTools.GetBacktestsPaginated(
|
||||
user,
|
||||
page,
|
||||
pageSize,
|
||||
sortBy,
|
||||
sortOrder,
|
||||
scoreMin,
|
||||
scoreMax,
|
||||
winrateMin,
|
||||
winrateMax,
|
||||
maxDrawdownMax,
|
||||
tickers,
|
||||
indicators,
|
||||
durationMinDays,
|
||||
durationMaxDays,
|
||||
name,
|
||||
tradingType);
|
||||
}
|
||||
|
||||
private T GetParameterValue<T>(Dictionary<string, object>? parameters, string key, T defaultValue)
|
||||
{
|
||||
if (parameters == null || !parameters.ContainsKey(key))
|
||||
{
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var value = parameters[key];
|
||||
if (value == null)
|
||||
{
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
// Handle nullable types
|
||||
var targetType = typeof(T);
|
||||
var underlyingType = Nullable.GetUnderlyingType(targetType);
|
||||
|
||||
if (underlyingType != null)
|
||||
{
|
||||
// It's a nullable type
|
||||
return (T)Convert.ChangeType(value, underlyingType);
|
||||
}
|
||||
|
||||
return (T)Convert.ChangeType(value, targetType);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
165
src/Managing.Application/LLM/Providers/ClaudeProvider.cs
Normal file
165
src/Managing.Application/LLM/Providers/ClaudeProvider.cs
Normal file
@@ -0,0 +1,165 @@
|
||||
using System.Net.Http.Json;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using Managing.Application.Abstractions.Services;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Managing.Application.LLM.Providers;
|
||||
|
||||
/// <summary>
|
||||
/// Anthropic Claude API provider
|
||||
/// </summary>
|
||||
public class ClaudeProvider : ILlmProvider
|
||||
{
|
||||
private readonly string _apiKey;
|
||||
private readonly string _defaultModel;
|
||||
private readonly HttpClient _httpClient;
|
||||
private readonly ILogger _logger;
|
||||
private const string BaseUrl = "https://api.anthropic.com/v1";
|
||||
private const string FallbackModel = "claude-3-5-sonnet-20241022";
|
||||
private const string AnthropicVersion = "2023-06-01";
|
||||
|
||||
public string Name => "claude";
|
||||
|
||||
public ClaudeProvider(string apiKey, string? defaultModel, IHttpClientFactory? httpClientFactory, ILogger logger)
|
||||
{
|
||||
_apiKey = apiKey;
|
||||
_defaultModel = defaultModel ?? FallbackModel;
|
||||
_httpClient = httpClientFactory?.CreateClient() ?? new HttpClient();
|
||||
_httpClient.DefaultRequestHeaders.Add("x-api-key", _apiKey);
|
||||
_httpClient.DefaultRequestHeaders.Add("anthropic-version", AnthropicVersion);
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task<LlmChatResponse> ChatAsync(LlmChatRequest request)
|
||||
{
|
||||
var url = $"{BaseUrl}/messages";
|
||||
|
||||
// Extract system message
|
||||
var systemMessage = request.Messages.FirstOrDefault(m => m.Role == "system")?.Content ?? "";
|
||||
var messages = request.Messages.Where(m => m.Role != "system").ToList();
|
||||
|
||||
var claudeRequest = new
|
||||
{
|
||||
model = _defaultModel,
|
||||
max_tokens = request.MaxTokens,
|
||||
temperature = request.Temperature,
|
||||
system = !string.IsNullOrWhiteSpace(systemMessage) ? systemMessage : null,
|
||||
messages = messages.Select(m => new
|
||||
{
|
||||
role = m.Role == "assistant" ? "assistant" : "user",
|
||||
content = m.Content
|
||||
}).ToArray(),
|
||||
tools = request.Tools?.Any() == true ? request.Tools.Select(t => new
|
||||
{
|
||||
name = t.Name,
|
||||
description = t.Description,
|
||||
input_schema = new
|
||||
{
|
||||
type = "object",
|
||||
properties = t.Parameters.ToDictionary(
|
||||
p => p.Key,
|
||||
p => new
|
||||
{
|
||||
type = p.Value.Type,
|
||||
description = p.Value.Description
|
||||
}
|
||||
),
|
||||
required = t.Parameters.Where(p => p.Value.Required).Select(p => p.Key).ToArray()
|
||||
}
|
||||
}).ToArray() : null
|
||||
};
|
||||
|
||||
var jsonOptions = new JsonSerializerOptions
|
||||
{
|
||||
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
|
||||
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
|
||||
};
|
||||
|
||||
var response = await _httpClient.PostAsJsonAsync(url, claudeRequest, jsonOptions);
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
var errorContent = await response.Content.ReadAsStringAsync();
|
||||
_logger.LogError("Claude API error: {StatusCode} - {Error}", response.StatusCode, errorContent);
|
||||
throw new HttpRequestException($"Claude API error: {response.StatusCode} - {errorContent}");
|
||||
}
|
||||
|
||||
var claudeResponse = await response.Content.ReadFromJsonAsync<ClaudeResponse>(jsonOptions);
|
||||
return ConvertFromClaudeResponse(claudeResponse!);
|
||||
}
|
||||
|
||||
private LlmChatResponse ConvertFromClaudeResponse(ClaudeResponse response)
|
||||
{
|
||||
var textContent = response.Content?.FirstOrDefault(c => c.Type == "text");
|
||||
var toolUseContents = response.Content?.Where(c => c.Type == "tool_use").ToList();
|
||||
|
||||
var llmResponse = new LlmChatResponse
|
||||
{
|
||||
Content = textContent?.Text ?? "",
|
||||
Provider = Name,
|
||||
Model = response.Model ?? _defaultModel,
|
||||
Usage = response.Usage != null ? new LlmUsage
|
||||
{
|
||||
PromptTokens = response.Usage.InputTokens,
|
||||
CompletionTokens = response.Usage.OutputTokens,
|
||||
TotalTokens = response.Usage.InputTokens + response.Usage.OutputTokens
|
||||
} : null
|
||||
};
|
||||
|
||||
if (toolUseContents?.Any() == true)
|
||||
{
|
||||
llmResponse.ToolCalls = toolUseContents.Select(tc => new LlmToolCall
|
||||
{
|
||||
Id = tc.Id ?? Guid.NewGuid().ToString(),
|
||||
Name = tc.Name ?? "",
|
||||
Arguments = tc.Input ?? new Dictionary<string, object>()
|
||||
}).ToList();
|
||||
llmResponse.RequiresToolExecution = true;
|
||||
}
|
||||
|
||||
return llmResponse;
|
||||
}
|
||||
|
||||
private class ClaudeResponse
|
||||
{
|
||||
[JsonPropertyName("id")]
|
||||
public string? Id { get; set; }
|
||||
|
||||
[JsonPropertyName("model")]
|
||||
public string? Model { get; set; }
|
||||
|
||||
[JsonPropertyName("content")]
|
||||
public List<ClaudeContent>? Content { get; set; }
|
||||
|
||||
[JsonPropertyName("usage")]
|
||||
public ClaudeUsage? Usage { get; set; }
|
||||
}
|
||||
|
||||
private class ClaudeContent
|
||||
{
|
||||
[JsonPropertyName("type")]
|
||||
public string Type { get; set; } = "";
|
||||
|
||||
[JsonPropertyName("text")]
|
||||
public string? Text { get; set; }
|
||||
|
||||
[JsonPropertyName("id")]
|
||||
public string? Id { get; set; }
|
||||
|
||||
[JsonPropertyName("name")]
|
||||
public string? Name { get; set; }
|
||||
|
||||
[JsonPropertyName("input")]
|
||||
public Dictionary<string, object>? Input { get; set; }
|
||||
}
|
||||
|
||||
private class ClaudeUsage
|
||||
{
|
||||
[JsonPropertyName("input_tokens")]
|
||||
public int InputTokens { get; set; }
|
||||
|
||||
[JsonPropertyName("output_tokens")]
|
||||
public int OutputTokens { get; set; }
|
||||
}
|
||||
}
|
||||
210
src/Managing.Application/LLM/Providers/GeminiProvider.cs
Normal file
210
src/Managing.Application/LLM/Providers/GeminiProvider.cs
Normal file
@@ -0,0 +1,210 @@
|
||||
using System.Net.Http.Json;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using Managing.Application.Abstractions.Services;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Managing.Application.LLM.Providers;
|
||||
|
||||
/// <summary>
|
||||
/// Google Gemini API provider
|
||||
/// </summary>
|
||||
public class GeminiProvider : ILlmProvider
|
||||
{
|
||||
private readonly string _apiKey;
|
||||
private readonly string _defaultModel;
|
||||
private readonly HttpClient _httpClient;
|
||||
private readonly ILogger _logger;
|
||||
private const string BaseUrl = "https://generativelanguage.googleapis.com/v1beta";
|
||||
private const string FallbackModel = "gemini-2.0-flash-exp";
|
||||
|
||||
public string Name => "gemini";
|
||||
|
||||
public GeminiProvider(string apiKey, string? defaultModel, IHttpClientFactory? httpClientFactory, ILogger logger)
|
||||
{
|
||||
_apiKey = apiKey;
|
||||
_defaultModel = defaultModel ?? FallbackModel;
|
||||
_httpClient = httpClientFactory?.CreateClient() ?? new HttpClient();
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task<LlmChatResponse> ChatAsync(LlmChatRequest request)
|
||||
{
|
||||
var model = _defaultModel;
|
||||
var url = $"{BaseUrl}/models/{model}:generateContent?key={_apiKey}";
|
||||
|
||||
var geminiRequest = ConvertToGeminiRequest(request);
|
||||
var jsonOptions = new JsonSerializerOptions
|
||||
{
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
|
||||
};
|
||||
|
||||
var response = await _httpClient.PostAsJsonAsync(url, geminiRequest, jsonOptions);
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
var errorContent = await response.Content.ReadAsStringAsync();
|
||||
_logger.LogError("Gemini API error: {StatusCode} - {Error}", response.StatusCode, errorContent);
|
||||
throw new HttpRequestException($"Gemini API error: {response.StatusCode} - {errorContent}");
|
||||
}
|
||||
|
||||
var geminiResponse = await response.Content.ReadFromJsonAsync<GeminiResponse>(jsonOptions);
|
||||
return ConvertFromGeminiResponse(geminiResponse!);
|
||||
}
|
||||
|
||||
private object ConvertToGeminiRequest(LlmChatRequest request)
|
||||
{
|
||||
var contents = request.Messages
|
||||
.Where(m => m.Role != "system") // Gemini doesn't support system messages in the same way
|
||||
.Select(m => new
|
||||
{
|
||||
role = m.Role == "assistant" ? "model" : "user",
|
||||
parts = new[]
|
||||
{
|
||||
new { text = m.Content }
|
||||
}
|
||||
}).ToList();
|
||||
|
||||
// Add system message as first user message if present
|
||||
var systemMessage = request.Messages.FirstOrDefault(m => m.Role == "system");
|
||||
if (systemMessage != null && !string.IsNullOrWhiteSpace(systemMessage.Content))
|
||||
{
|
||||
contents.Insert(0, new
|
||||
{
|
||||
role = "user",
|
||||
parts = new[]
|
||||
{
|
||||
new { text = $"System instructions: {systemMessage.Content}" }
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
var geminiRequest = new
|
||||
{
|
||||
contents,
|
||||
generationConfig = new
|
||||
{
|
||||
temperature = request.Temperature,
|
||||
maxOutputTokens = request.MaxTokens
|
||||
},
|
||||
tools = request.Tools?.Any() == true
|
||||
? new[]
|
||||
{
|
||||
new
|
||||
{
|
||||
functionDeclarations = request.Tools.Select(t => new
|
||||
{
|
||||
name = t.Name,
|
||||
description = t.Description,
|
||||
parameters = new
|
||||
{
|
||||
type = "object",
|
||||
properties = t.Parameters.ToDictionary(
|
||||
p => p.Key,
|
||||
p => new
|
||||
{
|
||||
type = p.Value.Type,
|
||||
description = p.Value.Description
|
||||
}
|
||||
),
|
||||
required = t.Parameters.Where(p => p.Value.Required).Select(p => p.Key).ToArray()
|
||||
}
|
||||
}).ToArray()
|
||||
}
|
||||
}
|
||||
: null
|
||||
};
|
||||
|
||||
return geminiRequest;
|
||||
}
|
||||
|
||||
private LlmChatResponse ConvertFromGeminiResponse(GeminiResponse response)
|
||||
{
|
||||
var candidate = response.Candidates?.FirstOrDefault();
|
||||
if (candidate == null)
|
||||
{
|
||||
return new LlmChatResponse
|
||||
{
|
||||
Content = "",
|
||||
Provider = Name,
|
||||
Model = _defaultModel
|
||||
};
|
||||
}
|
||||
|
||||
var content = candidate.Content;
|
||||
var textPart = content?.Parts?.FirstOrDefault(p => !string.IsNullOrWhiteSpace(p.Text));
|
||||
var functionCallParts = content?.Parts?.Where(p => p.FunctionCall != null).ToList();
|
||||
|
||||
var llmResponse = new LlmChatResponse
|
||||
{
|
||||
Content = textPart?.Text ?? "",
|
||||
Provider = Name,
|
||||
Model = _defaultModel,
|
||||
Usage = response.UsageMetadata != null
|
||||
? new LlmUsage
|
||||
{
|
||||
PromptTokens = response.UsageMetadata.PromptTokenCount,
|
||||
CompletionTokens = response.UsageMetadata.CandidatesTokenCount,
|
||||
TotalTokens = response.UsageMetadata.TotalTokenCount
|
||||
}
|
||||
: null
|
||||
};
|
||||
|
||||
// Handle function calls (tool calls)
|
||||
if (functionCallParts?.Any() == true)
|
||||
{
|
||||
llmResponse.ToolCalls = functionCallParts.Select((fc, idx) => new LlmToolCall
|
||||
{
|
||||
Id = $"call_{idx}",
|
||||
Name = fc.FunctionCall!.Name,
|
||||
Arguments = fc.FunctionCall.Args ?? new Dictionary<string, object>()
|
||||
}).ToList();
|
||||
llmResponse.RequiresToolExecution = true;
|
||||
}
|
||||
|
||||
return llmResponse;
|
||||
}
|
||||
|
||||
// Gemini API response models
|
||||
private class GeminiResponse
|
||||
{
|
||||
[JsonPropertyName("candidates")] public List<GeminiCandidate>? Candidates { get; set; }
|
||||
|
||||
[JsonPropertyName("usageMetadata")] public GeminiUsageMetadata? UsageMetadata { get; set; }
|
||||
}
|
||||
|
||||
private class GeminiCandidate
|
||||
{
|
||||
[JsonPropertyName("content")] public GeminiContent? Content { get; set; }
|
||||
}
|
||||
|
||||
private class GeminiContent
|
||||
{
|
||||
[JsonPropertyName("parts")] public List<GeminiPart>? Parts { get; set; }
|
||||
}
|
||||
|
||||
private class GeminiPart
|
||||
{
|
||||
[JsonPropertyName("text")] public string? Text { get; set; }
|
||||
|
||||
[JsonPropertyName("functionCall")] public GeminiFunctionCall? FunctionCall { get; set; }
|
||||
}
|
||||
|
||||
private class GeminiFunctionCall
|
||||
{
|
||||
[JsonPropertyName("name")] public string Name { get; set; } = "";
|
||||
|
||||
[JsonPropertyName("args")] public Dictionary<string, object>? Args { get; set; }
|
||||
}
|
||||
|
||||
private class GeminiUsageMetadata
|
||||
{
|
||||
[JsonPropertyName("promptTokenCount")] public int PromptTokenCount { get; set; }
|
||||
|
||||
[JsonPropertyName("candidatesTokenCount")]
|
||||
public int CandidatesTokenCount { get; set; }
|
||||
|
||||
[JsonPropertyName("totalTokenCount")] public int TotalTokenCount { get; set; }
|
||||
}
|
||||
}
|
||||
21
src/Managing.Application/LLM/Providers/ILlmProvider.cs
Normal file
21
src/Managing.Application/LLM/Providers/ILlmProvider.cs
Normal file
@@ -0,0 +1,21 @@
|
||||
using Managing.Application.Abstractions.Services;
|
||||
|
||||
namespace Managing.Application.LLM.Providers;
|
||||
|
||||
/// <summary>
|
||||
/// Interface for LLM provider implementations
|
||||
/// </summary>
|
||||
public interface ILlmProvider
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the name of the provider (e.g., "gemini", "openai", "claude")
|
||||
/// </summary>
|
||||
string Name { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Sends a chat request to the provider
|
||||
/// </summary>
|
||||
/// <param name="request">The chat request</param>
|
||||
/// <returns>The chat response</returns>
|
||||
Task<LlmChatResponse> ChatAsync(LlmChatRequest request);
|
||||
}
|
||||
199
src/Managing.Application/LLM/Providers/OpenAiProvider.cs
Normal file
199
src/Managing.Application/LLM/Providers/OpenAiProvider.cs
Normal file
@@ -0,0 +1,199 @@
|
||||
using System.Net.Http.Json;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using Managing.Application.Abstractions.Services;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Managing.Application.LLM.Providers;
|
||||
|
||||
/// <summary>
|
||||
/// OpenAI API provider
|
||||
/// </summary>
|
||||
public class OpenAiProvider : ILlmProvider
|
||||
{
|
||||
private readonly string _apiKey;
|
||||
private readonly string _defaultModel;
|
||||
private readonly HttpClient _httpClient;
|
||||
private readonly ILogger _logger;
|
||||
private const string BaseUrl = "https://api.openai.com/v1";
|
||||
private const string FallbackModel = "gpt-4o";
|
||||
|
||||
public string Name => "openai";
|
||||
|
||||
public OpenAiProvider(string apiKey, string? defaultModel, IHttpClientFactory? httpClientFactory, ILogger logger)
|
||||
{
|
||||
_apiKey = apiKey;
|
||||
_defaultModel = defaultModel ?? FallbackModel;
|
||||
_httpClient = httpClientFactory?.CreateClient() ?? new HttpClient();
|
||||
_httpClient.DefaultRequestHeaders.Add("Authorization", $"Bearer {_apiKey}");
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task<LlmChatResponse> ChatAsync(LlmChatRequest request)
|
||||
{
|
||||
var url = $"{BaseUrl}/chat/completions";
|
||||
|
||||
var openAiRequest = new
|
||||
{
|
||||
model = _defaultModel,
|
||||
messages = request.Messages.Select(m => new
|
||||
{
|
||||
role = m.Role,
|
||||
content = m.Content,
|
||||
tool_calls = m.ToolCalls?.Select(tc => new
|
||||
{
|
||||
id = tc.Id,
|
||||
type = "function",
|
||||
function = new
|
||||
{
|
||||
name = tc.Name,
|
||||
arguments = JsonSerializer.Serialize(tc.Arguments)
|
||||
}
|
||||
}),
|
||||
tool_call_id = m.ToolCallId
|
||||
}).ToArray(),
|
||||
temperature = request.Temperature,
|
||||
max_tokens = request.MaxTokens,
|
||||
tools = request.Tools?.Any() == true ? request.Tools.Select(t => new
|
||||
{
|
||||
type = "function",
|
||||
function = new
|
||||
{
|
||||
name = t.Name,
|
||||
description = t.Description,
|
||||
parameters = new
|
||||
{
|
||||
type = "object",
|
||||
properties = t.Parameters.ToDictionary(
|
||||
p => p.Key,
|
||||
p => new
|
||||
{
|
||||
type = p.Value.Type,
|
||||
description = p.Value.Description
|
||||
}
|
||||
),
|
||||
required = t.Parameters.Where(p => p.Value.Required).Select(p => p.Key).ToArray()
|
||||
}
|
||||
}
|
||||
}).ToArray() : null
|
||||
};
|
||||
|
||||
var jsonOptions = new JsonSerializerOptions
|
||||
{
|
||||
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
|
||||
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
|
||||
};
|
||||
|
||||
var response = await _httpClient.PostAsJsonAsync(url, openAiRequest, jsonOptions);
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
var errorContent = await response.Content.ReadAsStringAsync();
|
||||
_logger.LogError("OpenAI API error: {StatusCode} - {Error}", response.StatusCode, errorContent);
|
||||
throw new HttpRequestException($"OpenAI API error: {response.StatusCode} - {errorContent}");
|
||||
}
|
||||
|
||||
var openAiResponse = await response.Content.ReadFromJsonAsync<OpenAiResponse>(jsonOptions);
|
||||
return ConvertFromOpenAiResponse(openAiResponse!);
|
||||
}
|
||||
|
||||
private LlmChatResponse ConvertFromOpenAiResponse(OpenAiResponse response)
|
||||
{
|
||||
var choice = response.Choices?.FirstOrDefault();
|
||||
if (choice == null)
|
||||
{
|
||||
return new LlmChatResponse
|
||||
{
|
||||
Content = "",
|
||||
Provider = Name,
|
||||
Model = response.Model ?? _defaultModel
|
||||
};
|
||||
}
|
||||
|
||||
var llmResponse = new LlmChatResponse
|
||||
{
|
||||
Content = choice.Message?.Content ?? "",
|
||||
Provider = Name,
|
||||
Model = response.Model ?? _defaultModel,
|
||||
Usage = response.Usage != null ? new LlmUsage
|
||||
{
|
||||
PromptTokens = response.Usage.PromptTokens,
|
||||
CompletionTokens = response.Usage.CompletionTokens,
|
||||
TotalTokens = response.Usage.TotalTokens
|
||||
} : null
|
||||
};
|
||||
|
||||
if (choice.Message?.ToolCalls?.Any() == true)
|
||||
{
|
||||
llmResponse.ToolCalls = choice.Message.ToolCalls.Select(tc => new LlmToolCall
|
||||
{
|
||||
Id = tc.Id,
|
||||
Name = tc.Function.Name,
|
||||
Arguments = JsonSerializer.Deserialize<Dictionary<string, object>>(tc.Function.Arguments) ?? new()
|
||||
}).ToList();
|
||||
llmResponse.RequiresToolExecution = true;
|
||||
}
|
||||
|
||||
return llmResponse;
|
||||
}
|
||||
|
||||
private class OpenAiResponse
|
||||
{
|
||||
[JsonPropertyName("id")]
|
||||
public string? Id { get; set; }
|
||||
|
||||
[JsonPropertyName("model")]
|
||||
public string? Model { get; set; }
|
||||
|
||||
[JsonPropertyName("choices")]
|
||||
public List<OpenAiChoice>? Choices { get; set; }
|
||||
|
||||
[JsonPropertyName("usage")]
|
||||
public OpenAiUsage? Usage { get; set; }
|
||||
}
|
||||
|
||||
private class OpenAiChoice
|
||||
{
|
||||
[JsonPropertyName("message")]
|
||||
public OpenAiMessage? Message { get; set; }
|
||||
}
|
||||
|
||||
private class OpenAiMessage
|
||||
{
|
||||
[JsonPropertyName("content")]
|
||||
public string? Content { get; set; }
|
||||
|
||||
[JsonPropertyName("tool_calls")]
|
||||
public List<OpenAiToolCall>? ToolCalls { get; set; }
|
||||
}
|
||||
|
||||
private class OpenAiToolCall
|
||||
{
|
||||
[JsonPropertyName("id")]
|
||||
public string Id { get; set; } = "";
|
||||
|
||||
[JsonPropertyName("function")]
|
||||
public OpenAiFunction Function { get; set; } = new();
|
||||
}
|
||||
|
||||
private class OpenAiFunction
|
||||
{
|
||||
[JsonPropertyName("name")]
|
||||
public string Name { get; set; } = "";
|
||||
|
||||
[JsonPropertyName("arguments")]
|
||||
public string Arguments { get; set; } = "{}";
|
||||
}
|
||||
|
||||
private class OpenAiUsage
|
||||
{
|
||||
[JsonPropertyName("prompt_tokens")]
|
||||
public int PromptTokens { get; set; }
|
||||
|
||||
[JsonPropertyName("completion_tokens")]
|
||||
public int CompletionTokens { get; set; }
|
||||
|
||||
[JsonPropertyName("total_tokens")]
|
||||
public int TotalTokens { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -36,6 +36,7 @@
|
||||
<ProjectReference Include="..\Managing.Common\Managing.Common.csproj"/>
|
||||
<ProjectReference Include="..\Managing.Domain\Managing.Domain.csproj"/>
|
||||
<ProjectReference Include="..\Managing.Infrastructure.Database\Managing.Infrastructure.Databases.csproj"/>
|
||||
<ProjectReference Include="..\Managing.Mcp\Managing.Mcp.csproj"/>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -339,6 +339,22 @@ public class UserService : IUserService
|
||||
return user;
|
||||
}
|
||||
|
||||
public async Task<User> UpdateDefaultLlmProvider(User user, LlmProvider defaultLlmProvider)
|
||||
{
|
||||
user = await GetUserByName(user.Name);
|
||||
if (user.DefaultLlmProvider == defaultLlmProvider)
|
||||
return user;
|
||||
|
||||
// Update the default LLM provider on the provided user object
|
||||
user.DefaultLlmProvider = defaultLlmProvider;
|
||||
await _userRepository.SaveOrUpdateUserAsync(user);
|
||||
|
||||
_logger.LogInformation("Updated default LLM provider to {Provider} for user {UserId}",
|
||||
defaultLlmProvider, user.Id);
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
public async Task<User> UpdateUserSettings(User user, UserSettingsDto settings)
|
||||
{
|
||||
user = await GetUserByName(user.Name);
|
||||
|
||||
@@ -425,6 +425,11 @@ public static class ApiBootstrap
|
||||
// Admin services
|
||||
services.AddSingleton<IAdminConfigurationService, AdminConfigurationService>();
|
||||
|
||||
// LLM and MCP services
|
||||
services.AddScoped<ILlmService, Managing.Application.LLM.LlmService>();
|
||||
services.AddScoped<IMcpService, Managing.Application.LLM.McpService>();
|
||||
services.AddScoped<Managing.Mcp.Tools.BacktestTools>();
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
<ProjectReference Include="..\Managing.Infrastructure.Messengers\Managing.Infrastructure.Messengers.csproj"/>
|
||||
<ProjectReference Include="..\Managing.Infrastructure.Storage\Managing.Infrastructure.Storage.csproj"/>
|
||||
<ProjectReference Include="..\Managing.Infrastructure.Web3\Managing.Infrastructure.Evm.csproj"/>
|
||||
<ProjectReference Include="..\Managing.Mcp\Managing.Mcp.csproj"/>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -126,6 +126,14 @@ public static class Enums
|
||||
None
|
||||
}
|
||||
|
||||
public enum LlmProvider
|
||||
{
|
||||
Auto,
|
||||
Gemini,
|
||||
OpenAI,
|
||||
Claude
|
||||
}
|
||||
|
||||
public enum TradeDirection
|
||||
{
|
||||
None,
|
||||
|
||||
@@ -40,4 +40,7 @@ public class User
|
||||
[Id(17)] public decimal? SignalAgreementThreshold { get; set; }
|
||||
[Id(18)] public bool? AllowSignalTrendOverride { get; set; }
|
||||
[Id(19)] public TradingExchanges? DefaultExchange { get; set; }
|
||||
|
||||
// User Settings - LLM Configuration
|
||||
[Id(21)] public LlmProvider? DefaultLlmProvider { get; set; } = LlmProvider.Auto; // Default LLM provider
|
||||
}
|
||||
1797
src/Managing.Infrastructure.Database/Migrations/20260103140520_AddDefaultLlmProviderToUsers.Designer.cs
generated
Normal file
1797
src/Managing.Infrastructure.Database/Migrations/20260103140520_AddDefaultLlmProviderToUsers.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,38 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Managing.Infrastructure.Databases.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddDefaultLlmProviderToUsers : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
// Add column with default value
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "DefaultLlmProvider",
|
||||
table: "Users",
|
||||
type: "character varying(50)",
|
||||
maxLength: 50,
|
||||
nullable: true,
|
||||
defaultValue: "auto");
|
||||
|
||||
// Update existing NULL values to default
|
||||
migrationBuilder.Sql(@"
|
||||
UPDATE ""Users""
|
||||
SET ""DefaultLlmProvider"" = 'auto'
|
||||
WHERE ""DefaultLlmProvider"" IS NULL;
|
||||
");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "DefaultLlmProvider",
|
||||
table: "Users");
|
||||
}
|
||||
}
|
||||
}
|
||||
1796
src/Managing.Infrastructure.Database/Migrations/20260103141211_ConvertDefaultLlmProviderToEnum.Designer.cs
generated
Normal file
1796
src/Managing.Infrastructure.Database/Migrations/20260103141211_ConvertDefaultLlmProviderToEnum.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,57 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Managing.Infrastructure.Databases.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class ConvertDefaultLlmProviderToEnum : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
// Update existing "auto" values to "Auto" (enum format)
|
||||
migrationBuilder.Sql(@"
|
||||
UPDATE ""Users""
|
||||
SET ""DefaultLlmProvider"" = 'Auto'
|
||||
WHERE ""DefaultLlmProvider"" = 'auto' OR ""DefaultLlmProvider"" IS NULL;
|
||||
");
|
||||
|
||||
// Alter column to use enum format (stored as text, default "Auto")
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "DefaultLlmProvider",
|
||||
table: "Users",
|
||||
type: "text",
|
||||
nullable: true,
|
||||
defaultValueSql: "'Auto'",
|
||||
oldClrType: typeof(string),
|
||||
oldType: "character varying(50)",
|
||||
oldMaxLength: 50,
|
||||
oldNullable: true,
|
||||
oldDefaultValue: "auto");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
// Revert "Auto" values back to "auto" (lowercase)
|
||||
migrationBuilder.Sql(@"
|
||||
UPDATE ""Users""
|
||||
SET ""DefaultLlmProvider"" = 'auto'
|
||||
WHERE ""DefaultLlmProvider"" = 'Auto';
|
||||
");
|
||||
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "DefaultLlmProvider",
|
||||
table: "Users",
|
||||
type: "character varying(50)",
|
||||
maxLength: 50,
|
||||
nullable: true,
|
||||
defaultValue: "auto",
|
||||
oldClrType: typeof(string),
|
||||
oldType: "text",
|
||||
oldNullable: true,
|
||||
oldDefaultValueSql: "'Auto'");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1441,6 +1441,11 @@ namespace Managing.Infrastructure.Databases.Migrations
|
||||
b.Property<string>("DefaultExchange")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("DefaultLlmProvider")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("text")
|
||||
.HasDefaultValueSql("'Auto'");
|
||||
|
||||
b.Property<bool>("EnableAutoswap")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
|
||||
@@ -34,6 +34,9 @@ public class UserEntity
|
||||
[Column(TypeName = "decimal(5,4)")] public decimal? SignalAgreementThreshold { get; set; } = 0.5m; // Default: 50% agreement required
|
||||
public bool? AllowSignalTrendOverride { get; set; } = true; // Default: Allow signal strategies to override trends
|
||||
public TradingExchanges? DefaultExchange { get; set; } = TradingExchanges.GmxV2; // Default exchange
|
||||
|
||||
// User Settings - LLM Configuration
|
||||
public LlmProvider? DefaultLlmProvider { get; set; } = LlmProvider.Auto; // Default LLM provider
|
||||
|
||||
// Navigation properties
|
||||
public virtual ICollection<AccountEntity> Accounts { get; set; } = new List<AccountEntity>();
|
||||
|
||||
@@ -105,6 +105,9 @@ public class ManagingDbContext : DbContext
|
||||
.HasConversion<string>(); // Store enum as string
|
||||
entity.Property(e => e.DefaultExchange)
|
||||
.HasConversion<string>(); // Store enum as string
|
||||
entity.Property(e => e.DefaultLlmProvider)
|
||||
.HasConversion<string>() // Store enum as string
|
||||
.HasDefaultValueSql("'Auto'"); // Default LLM provider
|
||||
|
||||
// Create indexes for performance
|
||||
entity.HasIndex(e => e.Name).IsUnique();
|
||||
|
||||
@@ -146,6 +146,7 @@ public static class PostgreSqlMappers
|
||||
SignalAgreementThreshold = entity.SignalAgreementThreshold,
|
||||
AllowSignalTrendOverride = entity.AllowSignalTrendOverride,
|
||||
DefaultExchange = entity.DefaultExchange,
|
||||
DefaultLlmProvider = entity.DefaultLlmProvider,
|
||||
Accounts = entity.Accounts?.Select(MapAccountWithoutUser).ToList() ?? new List<Account>()
|
||||
};
|
||||
}
|
||||
@@ -193,7 +194,8 @@ public static class PostgreSqlMappers
|
||||
TrendStrongAgreementThreshold = user.TrendStrongAgreementThreshold,
|
||||
SignalAgreementThreshold = user.SignalAgreementThreshold,
|
||||
AllowSignalTrendOverride = user.AllowSignalTrendOverride,
|
||||
DefaultExchange = user.DefaultExchange
|
||||
DefaultExchange = user.DefaultExchange,
|
||||
DefaultLlmProvider = user.DefaultLlmProvider
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -269,6 +269,7 @@ public class PostgreSqlUserRepository : BaseRepositoryWithLogging, IUserReposito
|
||||
existingUser.SignalAgreementThreshold = user.SignalAgreementThreshold;
|
||||
existingUser.AllowSignalTrendOverride = user.AllowSignalTrendOverride;
|
||||
existingUser.DefaultExchange = user.DefaultExchange;
|
||||
existingUser.DefaultLlmProvider = user.DefaultLlmProvider;
|
||||
|
||||
_context.Users.Update(existingUser);
|
||||
|
||||
|
||||
20
src/Managing.Mcp/Managing.Mcp.csproj
Normal file
20
src/Managing.Mcp/Managing.Mcp.csproj
Normal file
@@ -0,0 +1,20 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.2"/>
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.3"/>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Managing.Application.Abstractions\Managing.Application.Abstractions.csproj"/>
|
||||
<ProjectReference Include="..\Managing.Common\Managing.Common.csproj"/>
|
||||
<ProjectReference Include="..\Managing.Domain\Managing.Domain.csproj"/>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
137
src/Managing.Mcp/Tools/BacktestTools.cs
Normal file
137
src/Managing.Mcp/Tools/BacktestTools.cs
Normal file
@@ -0,0 +1,137 @@
|
||||
using Managing.Application.Abstractions.Services;
|
||||
using Managing.Application.Abstractions.Shared;
|
||||
using Managing.Domain.Users;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using static Managing.Common.Enums;
|
||||
|
||||
namespace Managing.Mcp.Tools;
|
||||
|
||||
/// <summary>
|
||||
/// MCP tools for backtest operations
|
||||
/// </summary>
|
||||
public class BacktestTools
|
||||
{
|
||||
private readonly IBacktester _backtester;
|
||||
private readonly ILogger<BacktestTools> _logger;
|
||||
|
||||
public BacktestTools(IBacktester backtester, ILogger<BacktestTools> logger)
|
||||
{
|
||||
_backtester = backtester;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves paginated backtests for a user with filtering and sorting capabilities
|
||||
/// </summary>
|
||||
/// <param name="user">The user requesting the backtests</param>
|
||||
/// <param name="page">Page number (defaults to 1)</param>
|
||||
/// <param name="pageSize">Number of items per page (defaults to 50, max 100)</param>
|
||||
/// <param name="sortBy">Field to sort by (Score, WinRate, GrowthPercentage, etc.)</param>
|
||||
/// <param name="sortOrder">Sort order - "asc" or "desc" (defaults to "desc")</param>
|
||||
/// <param name="scoreMin">Minimum score filter (0-100)</param>
|
||||
/// <param name="scoreMax">Maximum score filter (0-100)</param>
|
||||
/// <param name="winrateMin">Minimum winrate filter (0-100)</param>
|
||||
/// <param name="winrateMax">Maximum winrate filter (0-100)</param>
|
||||
/// <param name="maxDrawdownMax">Maximum drawdown filter</param>
|
||||
/// <param name="tickers">Comma-separated list of tickers to filter by</param>
|
||||
/// <param name="indicators">Comma-separated list of indicators to filter by</param>
|
||||
/// <param name="durationMinDays">Minimum duration in days</param>
|
||||
/// <param name="durationMaxDays">Maximum duration in days</param>
|
||||
/// <param name="name">Name contains filter</param>
|
||||
/// <param name="tradingType">Trading type filter (Spot, Futures, etc.)</param>
|
||||
/// <returns>Paginated backtest results with metadata</returns>
|
||||
public async Task<object> GetBacktestsPaginated(
|
||||
User user,
|
||||
int page = 1,
|
||||
int pageSize = 50,
|
||||
BacktestSortableColumn sortBy = BacktestSortableColumn.Score,
|
||||
string sortOrder = "desc",
|
||||
double? scoreMin = null,
|
||||
double? scoreMax = null,
|
||||
int? winrateMin = null,
|
||||
int? winrateMax = null,
|
||||
decimal? maxDrawdownMax = null,
|
||||
string? tickers = null,
|
||||
string? indicators = null,
|
||||
double? durationMinDays = null,
|
||||
double? durationMaxDays = null,
|
||||
string? name = null,
|
||||
TradingType? tradingType = null)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Validate inputs
|
||||
if (page < 1) page = 1;
|
||||
if (pageSize < 1 || pageSize > 100) pageSize = 50;
|
||||
if (sortOrder != "asc" && sortOrder != "desc") sortOrder = "desc";
|
||||
|
||||
// Parse multi-selects if provided
|
||||
var tickerList = string.IsNullOrWhiteSpace(tickers)
|
||||
? Array.Empty<string>()
|
||||
: tickers.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
||||
var indicatorList = string.IsNullOrWhiteSpace(indicators)
|
||||
? Array.Empty<string>()
|
||||
: indicators.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
||||
|
||||
var filter = new BacktestsFilter
|
||||
{
|
||||
NameContains = string.IsNullOrWhiteSpace(name) ? null : name.Trim(),
|
||||
ScoreMin = scoreMin,
|
||||
ScoreMax = scoreMax,
|
||||
WinrateMin = winrateMin,
|
||||
WinrateMax = winrateMax,
|
||||
MaxDrawdownMax = maxDrawdownMax,
|
||||
Tickers = tickerList,
|
||||
Indicators = indicatorList,
|
||||
DurationMin = durationMinDays.HasValue ? TimeSpan.FromDays(durationMinDays.Value) : null,
|
||||
DurationMax = durationMaxDays.HasValue ? TimeSpan.FromDays(durationMaxDays.Value) : null,
|
||||
TradingType = tradingType
|
||||
};
|
||||
|
||||
var (backtests, totalCount) = await _backtester.GetBacktestsByUserPaginatedAsync(
|
||||
user,
|
||||
page,
|
||||
pageSize,
|
||||
sortBy,
|
||||
sortOrder,
|
||||
filter);
|
||||
|
||||
var totalPages = (int)Math.Ceiling(totalCount / (double)pageSize);
|
||||
|
||||
return new
|
||||
{
|
||||
Backtests = backtests.Select(b => new
|
||||
{
|
||||
b.Id,
|
||||
b.Config,
|
||||
b.FinalPnl,
|
||||
b.WinRate,
|
||||
b.GrowthPercentage,
|
||||
b.HodlPercentage,
|
||||
b.StartDate,
|
||||
b.EndDate,
|
||||
b.MaxDrawdown,
|
||||
b.Fees,
|
||||
b.SharpeRatio,
|
||||
b.Score,
|
||||
b.ScoreMessage,
|
||||
b.InitialBalance,
|
||||
b.NetPnl,
|
||||
b.PositionCount,
|
||||
TradingType = b.Config.TradingType
|
||||
}),
|
||||
TotalCount = totalCount,
|
||||
CurrentPage = page,
|
||||
PageSize = pageSize,
|
||||
TotalPages = totalPages,
|
||||
HasNextPage = page < totalPages,
|
||||
HasPreviousPage = page > 1
|
||||
};
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error getting paginated backtests for user {UserId}", user.Id);
|
||||
throw new InvalidOperationException($"Failed to retrieve backtests: {ex.Message}", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -61,6 +61,7 @@ export interface User {
|
||||
signalAgreementThreshold?: number | null;
|
||||
allowSignalTrendOverride?: boolean | null;
|
||||
defaultExchange?: TradingExchanges | null;
|
||||
defaultLlmProvider?: LlmProvider | null;
|
||||
}
|
||||
|
||||
export enum Confidence {
|
||||
@@ -70,6 +71,13 @@ export enum Confidence {
|
||||
None = "None",
|
||||
}
|
||||
|
||||
export enum LlmProvider {
|
||||
Auto = "Auto",
|
||||
Gemini = "Gemini",
|
||||
OpenAI = "OpenAI",
|
||||
Claude = "Claude",
|
||||
}
|
||||
|
||||
export interface Balance {
|
||||
tokenImage?: string | null;
|
||||
tokenName?: string | null;
|
||||
@@ -1435,6 +1443,57 @@ export interface JobStatusTypeSummary {
|
||||
count?: number;
|
||||
}
|
||||
|
||||
export interface LlmChatResponse {
|
||||
content?: string;
|
||||
provider?: string;
|
||||
model?: string;
|
||||
toolCalls?: LlmToolCall[] | null;
|
||||
usage?: LlmUsage | null;
|
||||
requiresToolExecution?: boolean;
|
||||
}
|
||||
|
||||
export interface LlmToolCall {
|
||||
id?: string;
|
||||
name?: string;
|
||||
arguments?: { [key: string]: any; };
|
||||
}
|
||||
|
||||
export interface LlmUsage {
|
||||
promptTokens?: number;
|
||||
completionTokens?: number;
|
||||
totalTokens?: number;
|
||||
}
|
||||
|
||||
export interface LlmChatRequest {
|
||||
messages?: LlmMessage[];
|
||||
provider?: string | null;
|
||||
apiKey?: string | null;
|
||||
stream?: boolean;
|
||||
temperature?: number;
|
||||
maxTokens?: number;
|
||||
tools?: McpToolDefinition[] | null;
|
||||
}
|
||||
|
||||
export interface LlmMessage {
|
||||
role?: string;
|
||||
content?: string;
|
||||
toolCalls?: LlmToolCall[] | null;
|
||||
toolCallId?: string | null;
|
||||
}
|
||||
|
||||
export interface McpToolDefinition {
|
||||
name?: string;
|
||||
description?: string;
|
||||
parameters?: { [key: string]: McpParameterDefinition; };
|
||||
}
|
||||
|
||||
export interface McpParameterDefinition {
|
||||
type?: string;
|
||||
description?: string;
|
||||
required?: boolean;
|
||||
defaultValue?: any | null;
|
||||
}
|
||||
|
||||
export interface ScenarioViewModel {
|
||||
name: string;
|
||||
indicators: IndicatorViewModel[];
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Auth } from '../pages/authPage/auth'
|
||||
import AiChatButton from '../components/organism/AiChatButton'
|
||||
|
||||
import MyRoutes from './routes'
|
||||
|
||||
@@ -6,6 +7,7 @@ const App = () => {
|
||||
return (
|
||||
<Auth>
|
||||
<MyRoutes />
|
||||
<AiChatButton />
|
||||
</Auth>
|
||||
)
|
||||
}
|
||||
|
||||
224
src/Managing.WebApp/src/components/organism/AiChat.tsx
Normal file
224
src/Managing.WebApp/src/components/organism/AiChat.tsx
Normal file
@@ -0,0 +1,224 @@
|
||||
import { useState, useRef, useEffect } from 'react'
|
||||
import { LlmClient } from '../../generated/ManagingApi'
|
||||
import { LlmMessage, LlmChatResponse } from '../../generated/ManagingApiTypes'
|
||||
import { AiChatService } from '../../services/aiChatService'
|
||||
import useApiUrlStore from '../../app/store/apiStore'
|
||||
|
||||
interface Message {
|
||||
role: 'user' | 'assistant' | 'system'
|
||||
content: string
|
||||
timestamp: Date
|
||||
}
|
||||
|
||||
interface AiChatProps {
|
||||
onClose?: () => void
|
||||
}
|
||||
|
||||
function AiChat({ onClose }: AiChatProps): JSX.Element {
|
||||
const [messages, setMessages] = useState<Message[]>([
|
||||
{
|
||||
role: 'system',
|
||||
content: 'You are a helpful AI assistant for the Managing trading platform. You can help users query their backtests, analyze trading strategies, and provide insights.',
|
||||
timestamp: new Date()
|
||||
}
|
||||
])
|
||||
const [input, setInput] = useState('')
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const [provider, setProvider] = useState<string>('auto')
|
||||
const [availableProviders, setAvailableProviders] = useState<string[]>([])
|
||||
const messagesEndRef = useRef<HTMLDivElement>(null)
|
||||
const { apiUrl, userToken } = useApiUrlStore()
|
||||
|
||||
useEffect(() => {
|
||||
scrollToBottom()
|
||||
}, [messages])
|
||||
|
||||
useEffect(() => {
|
||||
loadProviders()
|
||||
}, [])
|
||||
|
||||
const scrollToBottom = () => {
|
||||
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' })
|
||||
}
|
||||
|
||||
const loadProviders = async () => {
|
||||
try {
|
||||
const llmClient = new LlmClient({}, apiUrl)
|
||||
const service = new AiChatService(llmClient)
|
||||
const providers = await service.getProviders()
|
||||
setAvailableProviders(['auto', ...providers])
|
||||
} catch (error) {
|
||||
console.error('Failed to load providers:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const sendMessage = async () => {
|
||||
if (!input.trim() || isLoading) return
|
||||
|
||||
const userMessage: Message = {
|
||||
role: 'user',
|
||||
content: input,
|
||||
timestamp: new Date()
|
||||
}
|
||||
|
||||
setMessages(prev => [...prev, userMessage])
|
||||
setInput('')
|
||||
setIsLoading(true)
|
||||
|
||||
try {
|
||||
const llmClient = new LlmClient({}, apiUrl)
|
||||
const service = new AiChatService(llmClient)
|
||||
|
||||
// Convert messages to LlmMessage format
|
||||
const llmMessages: LlmMessage[] = messages
|
||||
.filter(m => m.role !== 'system' || messages.indexOf(m) === 0) // Include only first system message
|
||||
.map(m => ({
|
||||
role: m.role,
|
||||
content: m.content,
|
||||
toolCalls: undefined,
|
||||
toolCallId: undefined
|
||||
}))
|
||||
|
||||
// Add the new user message
|
||||
llmMessages.push({
|
||||
role: 'user',
|
||||
content: input,
|
||||
toolCalls: undefined,
|
||||
toolCallId: undefined
|
||||
})
|
||||
|
||||
const response: LlmChatResponse = await service.sendMessage(
|
||||
llmMessages,
|
||||
provider === 'auto' ? undefined : provider
|
||||
)
|
||||
|
||||
const assistantMessage: Message = {
|
||||
role: 'assistant',
|
||||
content: response.content || 'No response from AI',
|
||||
timestamp: new Date()
|
||||
}
|
||||
|
||||
setMessages(prev => [...prev, assistantMessage])
|
||||
} catch (error: any) {
|
||||
console.error('Error sending message:', error)
|
||||
const errorMessage: Message = {
|
||||
role: 'assistant',
|
||||
content: `Error: ${error?.message || 'Failed to get response from AI'}`,
|
||||
timestamp: new Date()
|
||||
}
|
||||
setMessages(prev => [...prev, errorMessage])
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleKeyPress = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
e.preventDefault()
|
||||
sendMessage()
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full bg-base-100">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between p-4 border-b border-base-300">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-8 h-8 bg-primary rounded-full flex items-center justify-center">
|
||||
<svg className="w-5 h-5 text-primary-content" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="font-bold text-lg">AI Assistant</h2>
|
||||
<p className="text-sm text-base-content/60">Powered by MCP</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{/* Provider Selection */}
|
||||
<select
|
||||
value={provider}
|
||||
onChange={(e) => setProvider(e.target.value)}
|
||||
className="select select-sm select-bordered"
|
||||
disabled={isLoading}
|
||||
>
|
||||
{availableProviders.map(p => (
|
||||
<option key={p} value={p}>
|
||||
{p === 'auto' ? 'Auto (Backend Selects)' : p.charAt(0).toUpperCase() + p.slice(1)}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
{onClose && (
|
||||
<button onClick={onClose} className="btn btn-sm btn-ghost btn-circle">
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Messages */}
|
||||
<div className="flex-1 overflow-y-auto p-4 space-y-4">
|
||||
{messages.filter(m => m.role !== 'system').map((message, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className={`flex ${message.role === 'user' ? 'justify-end' : 'justify-start'}`}
|
||||
>
|
||||
<div
|
||||
className={`max-w-[80%] p-3 rounded-lg ${
|
||||
message.role === 'user'
|
||||
? 'bg-primary text-primary-content'
|
||||
: 'bg-base-200 text-base-content'
|
||||
}`}
|
||||
>
|
||||
<p className="whitespace-pre-wrap break-words">{message.content}</p>
|
||||
<p className="text-xs opacity-60 mt-1">
|
||||
{message.timestamp.toLocaleTimeString()}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
{isLoading && (
|
||||
<div className="flex justify-start">
|
||||
<div className="bg-base-200 p-3 rounded-lg">
|
||||
<div className="flex gap-1">
|
||||
<span className="loading loading-dots loading-sm"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div ref={messagesEndRef} />
|
||||
</div>
|
||||
|
||||
{/* Input */}
|
||||
<div className="p-4 border-t border-base-300">
|
||||
<div className="flex gap-2">
|
||||
<textarea
|
||||
value={input}
|
||||
onChange={(e) => setInput(e.target.value)}
|
||||
onKeyPress={handleKeyPress}
|
||||
placeholder="Ask me anything about your backtests..."
|
||||
className="textarea textarea-bordered flex-1 resize-none"
|
||||
rows={2}
|
||||
disabled={isLoading}
|
||||
/>
|
||||
<button
|
||||
onClick={sendMessage}
|
||||
disabled={isLoading || !input.trim()}
|
||||
className="btn btn-primary"
|
||||
>
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 19l9 2-9-18-9 18 9-2zm0 0v-8" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<p className="text-xs text-base-content/60 mt-2">
|
||||
Press Enter to send, Shift+Enter for new line
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default AiChat
|
||||
32
src/Managing.WebApp/src/components/organism/AiChatButton.tsx
Normal file
32
src/Managing.WebApp/src/components/organism/AiChatButton.tsx
Normal file
@@ -0,0 +1,32 @@
|
||||
import { useState } from 'react'
|
||||
import AiChat from './AiChat'
|
||||
|
||||
function AiChatButton(): JSX.Element {
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Floating Chat Button */}
|
||||
{!isOpen && (
|
||||
<button
|
||||
onClick={() => setIsOpen(true)}
|
||||
className="fixed bottom-6 right-6 btn btn-circle btn-primary btn-lg shadow-lg z-50 hover:scale-110 transition-transform"
|
||||
aria-label="Open AI Chat"
|
||||
>
|
||||
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 10h.01M12 10h.01M16 10h.01M9 16H5a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v8a2 2 0 01-2 2h-5l-5 5v-5z" />
|
||||
</svg>
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* Chat Window */}
|
||||
{isOpen && (
|
||||
<div className="fixed bottom-6 right-6 w-[400px] h-[600px] bg-base-100 rounded-lg shadow-2xl z-50 border border-base-300 flex flex-col overflow-hidden">
|
||||
<AiChat onClose={() => setIsOpen(false)} />
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default AiChatButton
|
||||
@@ -2899,6 +2899,127 @@ export class JobClient extends AuthorizedApiBase {
|
||||
}
|
||||
}
|
||||
|
||||
export class LlmClient extends AuthorizedApiBase {
|
||||
private http: { fetch(url: RequestInfo, init?: RequestInit): Promise<Response> };
|
||||
private baseUrl: string;
|
||||
protected jsonParseReviver: ((key: string, value: any) => any) | undefined = undefined;
|
||||
|
||||
constructor(configuration: IConfig, baseUrl?: string, http?: { fetch(url: RequestInfo, init?: RequestInit): Promise<Response> }) {
|
||||
super(configuration);
|
||||
this.http = http ? http : window as any;
|
||||
this.baseUrl = baseUrl ?? "http://localhost:5000";
|
||||
}
|
||||
|
||||
llm_Chat(request: LlmChatRequest): Promise<LlmChatResponse> {
|
||||
let url_ = this.baseUrl + "/Llm/Chat";
|
||||
url_ = url_.replace(/[?&]$/, "");
|
||||
|
||||
const content_ = JSON.stringify(request);
|
||||
|
||||
let options_: RequestInit = {
|
||||
body: content_,
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Accept": "application/json"
|
||||
}
|
||||
};
|
||||
|
||||
return this.transformOptions(options_).then(transformedOptions_ => {
|
||||
return this.http.fetch(url_, transformedOptions_);
|
||||
}).then((_response: Response) => {
|
||||
return this.processLlm_Chat(_response);
|
||||
});
|
||||
}
|
||||
|
||||
protected processLlm_Chat(response: Response): Promise<LlmChatResponse> {
|
||||
const status = response.status;
|
||||
let _headers: any = {}; if (response.headers && response.headers.forEach) { response.headers.forEach((v: any, k: any) => _headers[k] = v); };
|
||||
if (status === 200) {
|
||||
return response.text().then((_responseText) => {
|
||||
let result200: any = null;
|
||||
result200 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver) as LlmChatResponse;
|
||||
return result200;
|
||||
});
|
||||
} else if (status !== 200 && status !== 204) {
|
||||
return response.text().then((_responseText) => {
|
||||
return throwException("An unexpected server error occurred.", status, _responseText, _headers);
|
||||
});
|
||||
}
|
||||
return Promise.resolve<LlmChatResponse>(null as any);
|
||||
}
|
||||
|
||||
llm_GetProviders(): Promise<string[]> {
|
||||
let url_ = this.baseUrl + "/Llm/Providers";
|
||||
url_ = url_.replace(/[?&]$/, "");
|
||||
|
||||
let options_: RequestInit = {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Accept": "application/json"
|
||||
}
|
||||
};
|
||||
|
||||
return this.transformOptions(options_).then(transformedOptions_ => {
|
||||
return this.http.fetch(url_, transformedOptions_);
|
||||
}).then((_response: Response) => {
|
||||
return this.processLlm_GetProviders(_response);
|
||||
});
|
||||
}
|
||||
|
||||
protected processLlm_GetProviders(response: Response): Promise<string[]> {
|
||||
const status = response.status;
|
||||
let _headers: any = {}; if (response.headers && response.headers.forEach) { response.headers.forEach((v: any, k: any) => _headers[k] = v); };
|
||||
if (status === 200) {
|
||||
return response.text().then((_responseText) => {
|
||||
let result200: any = null;
|
||||
result200 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver) as string[];
|
||||
return result200;
|
||||
});
|
||||
} else if (status !== 200 && status !== 204) {
|
||||
return response.text().then((_responseText) => {
|
||||
return throwException("An unexpected server error occurred.", status, _responseText, _headers);
|
||||
});
|
||||
}
|
||||
return Promise.resolve<string[]>(null as any);
|
||||
}
|
||||
|
||||
llm_GetTools(): Promise<McpToolDefinition[]> {
|
||||
let url_ = this.baseUrl + "/Llm/Tools";
|
||||
url_ = url_.replace(/[?&]$/, "");
|
||||
|
||||
let options_: RequestInit = {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Accept": "application/json"
|
||||
}
|
||||
};
|
||||
|
||||
return this.transformOptions(options_).then(transformedOptions_ => {
|
||||
return this.http.fetch(url_, transformedOptions_);
|
||||
}).then((_response: Response) => {
|
||||
return this.processLlm_GetTools(_response);
|
||||
});
|
||||
}
|
||||
|
||||
protected processLlm_GetTools(response: Response): Promise<McpToolDefinition[]> {
|
||||
const status = response.status;
|
||||
let _headers: any = {}; if (response.headers && response.headers.forEach) { response.headers.forEach((v: any, k: any) => _headers[k] = v); };
|
||||
if (status === 200) {
|
||||
return response.text().then((_responseText) => {
|
||||
let result200: any = null;
|
||||
result200 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver) as McpToolDefinition[];
|
||||
return result200;
|
||||
});
|
||||
} else if (status !== 200 && status !== 204) {
|
||||
return response.text().then((_responseText) => {
|
||||
return throwException("An unexpected server error occurred.", status, _responseText, _headers);
|
||||
});
|
||||
}
|
||||
return Promise.resolve<McpToolDefinition[]>(null as any);
|
||||
}
|
||||
}
|
||||
|
||||
export class MoneyManagementClient extends AuthorizedApiBase {
|
||||
private http: { fetch(url: RequestInfo, init?: RequestInit): Promise<Response> };
|
||||
private baseUrl: string;
|
||||
@@ -4388,6 +4509,45 @@ export class UserClient extends AuthorizedApiBase {
|
||||
return Promise.resolve<User>(null as any);
|
||||
}
|
||||
|
||||
user_UpdateDefaultLlmProvider(defaultLlmProvider: string): Promise<User> {
|
||||
let url_ = this.baseUrl + "/User/default-llm-provider";
|
||||
url_ = url_.replace(/[?&]$/, "");
|
||||
|
||||
const content_ = JSON.stringify(defaultLlmProvider);
|
||||
|
||||
let options_: RequestInit = {
|
||||
body: content_,
|
||||
method: "PUT",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Accept": "application/json"
|
||||
}
|
||||
};
|
||||
|
||||
return this.transformOptions(options_).then(transformedOptions_ => {
|
||||
return this.http.fetch(url_, transformedOptions_);
|
||||
}).then((_response: Response) => {
|
||||
return this.processUser_UpdateDefaultLlmProvider(_response);
|
||||
});
|
||||
}
|
||||
|
||||
protected processUser_UpdateDefaultLlmProvider(response: Response): Promise<User> {
|
||||
const status = response.status;
|
||||
let _headers: any = {}; if (response.headers && response.headers.forEach) { response.headers.forEach((v: any, k: any) => _headers[k] = v); };
|
||||
if (status === 200) {
|
||||
return response.text().then((_responseText) => {
|
||||
let result200: any = null;
|
||||
result200 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver) as User;
|
||||
return result200;
|
||||
});
|
||||
} else if (status !== 200 && status !== 204) {
|
||||
return response.text().then((_responseText) => {
|
||||
return throwException("An unexpected server error occurred.", status, _responseText, _headers);
|
||||
});
|
||||
}
|
||||
return Promise.resolve<User>(null as any);
|
||||
}
|
||||
|
||||
user_TestTelegramChannel(): Promise<string> {
|
||||
let url_ = this.baseUrl + "/User/telegram-channel/test";
|
||||
url_ = url_.replace(/[?&]$/, "");
|
||||
@@ -4690,6 +4850,7 @@ export interface User {
|
||||
signalAgreementThreshold?: number | null;
|
||||
allowSignalTrendOverride?: boolean | null;
|
||||
defaultExchange?: TradingExchanges | null;
|
||||
defaultLlmProvider?: LlmProvider | null;
|
||||
}
|
||||
|
||||
export enum Confidence {
|
||||
@@ -4699,6 +4860,13 @@ export enum Confidence {
|
||||
None = "None",
|
||||
}
|
||||
|
||||
export enum LlmProvider {
|
||||
Auto = "Auto",
|
||||
Gemini = "Gemini",
|
||||
OpenAI = "OpenAI",
|
||||
Claude = "Claude",
|
||||
}
|
||||
|
||||
export interface Balance {
|
||||
tokenImage?: string | null;
|
||||
tokenName?: string | null;
|
||||
@@ -6064,6 +6232,57 @@ export interface JobStatusTypeSummary {
|
||||
count?: number;
|
||||
}
|
||||
|
||||
export interface LlmChatResponse {
|
||||
content?: string;
|
||||
provider?: string;
|
||||
model?: string;
|
||||
toolCalls?: LlmToolCall[] | null;
|
||||
usage?: LlmUsage | null;
|
||||
requiresToolExecution?: boolean;
|
||||
}
|
||||
|
||||
export interface LlmToolCall {
|
||||
id?: string;
|
||||
name?: string;
|
||||
arguments?: { [key: string]: any; };
|
||||
}
|
||||
|
||||
export interface LlmUsage {
|
||||
promptTokens?: number;
|
||||
completionTokens?: number;
|
||||
totalTokens?: number;
|
||||
}
|
||||
|
||||
export interface LlmChatRequest {
|
||||
messages?: LlmMessage[];
|
||||
provider?: string | null;
|
||||
apiKey?: string | null;
|
||||
stream?: boolean;
|
||||
temperature?: number;
|
||||
maxTokens?: number;
|
||||
tools?: McpToolDefinition[] | null;
|
||||
}
|
||||
|
||||
export interface LlmMessage {
|
||||
role?: string;
|
||||
content?: string;
|
||||
toolCalls?: LlmToolCall[] | null;
|
||||
toolCallId?: string | null;
|
||||
}
|
||||
|
||||
export interface McpToolDefinition {
|
||||
name?: string;
|
||||
description?: string;
|
||||
parameters?: { [key: string]: McpParameterDefinition; };
|
||||
}
|
||||
|
||||
export interface McpParameterDefinition {
|
||||
type?: string;
|
||||
description?: string;
|
||||
required?: boolean;
|
||||
defaultValue?: any | null;
|
||||
}
|
||||
|
||||
export interface ScenarioViewModel {
|
||||
name: string;
|
||||
indicators: IndicatorViewModel[];
|
||||
|
||||
@@ -61,6 +61,7 @@ export interface User {
|
||||
signalAgreementThreshold?: number | null;
|
||||
allowSignalTrendOverride?: boolean | null;
|
||||
defaultExchange?: TradingExchanges | null;
|
||||
defaultLlmProvider?: LlmProvider | null;
|
||||
}
|
||||
|
||||
export enum Confidence {
|
||||
@@ -70,6 +71,13 @@ export enum Confidence {
|
||||
None = "None",
|
||||
}
|
||||
|
||||
export enum LlmProvider {
|
||||
Auto = "Auto",
|
||||
Gemini = "Gemini",
|
||||
OpenAI = "OpenAI",
|
||||
Claude = "Claude",
|
||||
}
|
||||
|
||||
export interface Balance {
|
||||
tokenImage?: string | null;
|
||||
tokenName?: string | null;
|
||||
@@ -1435,6 +1443,57 @@ export interface JobStatusTypeSummary {
|
||||
count?: number;
|
||||
}
|
||||
|
||||
export interface LlmChatResponse {
|
||||
content?: string;
|
||||
provider?: string;
|
||||
model?: string;
|
||||
toolCalls?: LlmToolCall[] | null;
|
||||
usage?: LlmUsage | null;
|
||||
requiresToolExecution?: boolean;
|
||||
}
|
||||
|
||||
export interface LlmToolCall {
|
||||
id?: string;
|
||||
name?: string;
|
||||
arguments?: { [key: string]: any; };
|
||||
}
|
||||
|
||||
export interface LlmUsage {
|
||||
promptTokens?: number;
|
||||
completionTokens?: number;
|
||||
totalTokens?: number;
|
||||
}
|
||||
|
||||
export interface LlmChatRequest {
|
||||
messages?: LlmMessage[];
|
||||
provider?: string | null;
|
||||
apiKey?: string | null;
|
||||
stream?: boolean;
|
||||
temperature?: number;
|
||||
maxTokens?: number;
|
||||
tools?: McpToolDefinition[] | null;
|
||||
}
|
||||
|
||||
export interface LlmMessage {
|
||||
role?: string;
|
||||
content?: string;
|
||||
toolCalls?: LlmToolCall[] | null;
|
||||
toolCallId?: string | null;
|
||||
}
|
||||
|
||||
export interface McpToolDefinition {
|
||||
name?: string;
|
||||
description?: string;
|
||||
parameters?: { [key: string]: McpParameterDefinition; };
|
||||
}
|
||||
|
||||
export interface McpParameterDefinition {
|
||||
type?: string;
|
||||
description?: string;
|
||||
required?: boolean;
|
||||
defaultValue?: any | null;
|
||||
}
|
||||
|
||||
export interface ScenarioViewModel {
|
||||
name: string;
|
||||
indicators: IndicatorViewModel[];
|
||||
|
||||
43
src/Managing.WebApp/src/services/aiChatService.ts
Normal file
43
src/Managing.WebApp/src/services/aiChatService.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { LlmClient } from '../generated/ManagingApi'
|
||||
import { LlmChatRequest, LlmChatResponse, LlmMessage } from '../generated/ManagingApiTypes'
|
||||
|
||||
export class AiChatService {
|
||||
private llmClient: LlmClient
|
||||
|
||||
constructor(llmClient: LlmClient) {
|
||||
this.llmClient = llmClient
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a chat message to the AI with MCP tool calling support
|
||||
*/
|
||||
async sendMessage(messages: LlmMessage[], provider?: string, apiKey?: string): Promise<LlmChatResponse> {
|
||||
const request: LlmChatRequest = {
|
||||
messages,
|
||||
provider: provider || 'auto',
|
||||
apiKey: apiKey,
|
||||
stream: false,
|
||||
temperature: 0.7,
|
||||
maxTokens: 4096,
|
||||
tools: undefined // Will be populated by backend
|
||||
}
|
||||
|
||||
return await this.llmClient.llm_Chat(request)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get available LLM providers
|
||||
*/
|
||||
async getProviders(): Promise<string[]> {
|
||||
return await this.llmClient.llm_GetProviders()
|
||||
}
|
||||
|
||||
/**
|
||||
* Get available MCP tools
|
||||
*/
|
||||
async getTools(): Promise<any[]> {
|
||||
return await this.llmClient.llm_GetTools()
|
||||
}
|
||||
}
|
||||
|
||||
export default AiChatService
|
||||
@@ -72,6 +72,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Managing.Domain.Tests", "Ma
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Managing.AppHost", "Managing.AppHost\Managing.AppHost.csproj", "{4712128B-F222-47C4-A347-AFF4E5BA02AE}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Managing.Mcp", "Managing.Mcp\Managing.Mcp.csproj", "{601B1A5B-568A-4238-AB93-78390FC52D91}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
@@ -256,6 +258,14 @@ Global
|
||||
{4712128B-F222-47C4-A347-AFF4E5BA02AE}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{4712128B-F222-47C4-A347-AFF4E5BA02AE}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{4712128B-F222-47C4-A347-AFF4E5BA02AE}.Release|x64.Build.0 = Release|Any CPU
|
||||
{601B1A5B-568A-4238-AB93-78390FC52D91}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{601B1A5B-568A-4238-AB93-78390FC52D91}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{601B1A5B-568A-4238-AB93-78390FC52D91}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{601B1A5B-568A-4238-AB93-78390FC52D91}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{601B1A5B-568A-4238-AB93-78390FC52D91}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{601B1A5B-568A-4238-AB93-78390FC52D91}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{601B1A5B-568A-4238-AB93-78390FC52D91}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{601B1A5B-568A-4238-AB93-78390FC52D91}.Release|x64.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
@@ -281,6 +291,7 @@ Global
|
||||
{B7D66A73-CA3A-4DE5-8E88-59D50C4018A6} = {A1296069-2816-43D4-882C-516BCB718D03}
|
||||
{55B059EF-F128-453F-B678-0FF00F1D2E95} = {8F2ECEA7-5BCA-45DF-B6E3-88AADD7AFD45}
|
||||
{3F835B88-4720-49C2-A4A5-FED2C860C4C4} = {8F2ECEA7-5BCA-45DF-B6E3-88AADD7AFD45}
|
||||
{601B1A5B-568A-4238-AB93-78390FC52D91} = {A1296069-2816-43D4-882C-516BCB718D03}
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {BD7CA081-CE52-4824-9777-C0562E54F3EA}
|
||||
|
||||
Reference in New Issue
Block a user