Add full monorepo: virtual-banker, backend, frontend, docs, scripts, deployment

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
defiQUG
2026-02-10 11:32:49 -08:00
commit b4753cef7e
81 changed files with 9255 additions and 0 deletions

View File

@@ -0,0 +1,68 @@
package banking
import (
"context"
"github.com/explorer/virtual-banker/backend/tools"
)
// AccountStatusTool gets account status
type AccountStatusTool struct {
client *BankingClient
}
// NewAccountStatusTool creates a new account status tool
func NewAccountStatusTool() *AccountStatusTool {
return &AccountStatusTool{
client: NewBankingClient(getBankingAPIURL()),
}
}
// getBankingAPIURL gets the banking API URL from environment
func getBankingAPIURL() string {
// Default to main API URL
return "http://localhost:8080"
}
// Name returns the tool name
func (t *AccountStatusTool) Name() string {
return "get_account_status"
}
// Description returns the tool description
func (t *AccountStatusTool) Description() string {
return "Get the status of a bank account including balance, transactions, and account details"
}
// Execute executes the tool
func (t *AccountStatusTool) Execute(ctx context.Context, params map[string]interface{}) (*tools.ToolResult, error) {
accountID, ok := params["account_id"].(string)
if !ok || accountID == "" {
return &tools.ToolResult{
Success: false,
Error: "account_id is required",
}, nil
}
// Call banking service
data, err := t.client.GetAccountStatus(ctx, accountID)
if err != nil {
// Fallback to mock data if service unavailable
return &tools.ToolResult{
Success: true,
Data: map[string]interface{}{
"account_id": accountID,
"balance": 10000.00,
"currency": "USD",
"status": "active",
"type": "checking",
"note": "Using fallback data - banking service unavailable",
},
}, nil
}
return &tools.ToolResult{
Success: true,
Data: data,
}, nil
}

View File

@@ -0,0 +1,66 @@
package banking
import (
"context"
"fmt"
"github.com/explorer/virtual-banker/backend/tools"
)
// CreateTicketTool creates a support ticket
type CreateTicketTool struct {
client *BankingClient
}
// NewCreateTicketTool creates a new create ticket tool
func NewCreateTicketTool() *CreateTicketTool {
return &CreateTicketTool{
client: NewBankingClient(getBankingAPIURL()),
}
}
// Name returns the tool name
func (t *CreateTicketTool) Name() string {
return "create_support_ticket"
}
// Description returns the tool description
func (t *CreateTicketTool) Description() string {
return "Create a support ticket for customer service"
}
// Execute executes the tool
func (t *CreateTicketTool) Execute(ctx context.Context, params map[string]interface{}) (*tools.ToolResult, error) {
subject, _ := params["subject"].(string)
details, _ := params["details"].(string)
if subject == "" {
return &tools.ToolResult{
Success: false,
Error: "subject is required",
}, nil
}
// Call banking service
data, err := t.client.CreateTicket(ctx, subject, details)
if err != nil {
// Fallback to mock data if service unavailable
return &tools.ToolResult{
Success: true,
Data: map[string]interface{}{
"ticket_id": fmt.Sprintf("TKT-%d", 12345),
"subject": subject,
"status": "open",
"note": "Using fallback data - banking service unavailable",
},
RequiresConfirmation: false,
}, nil
}
return &tools.ToolResult{
Success: true,
Data: data,
RequiresConfirmation: false,
}, nil
}

View File

@@ -0,0 +1,91 @@
package banking
import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
"time"
)
// BankingClient provides access to backend banking services
type BankingClient struct {
baseURL string
httpClient *http.Client
}
// NewBankingClient creates a new banking client
func NewBankingClient(baseURL string) *BankingClient {
return &BankingClient{
baseURL: baseURL,
httpClient: &http.Client{
Timeout: 10 * time.Second,
},
}
}
// GetAccountStatus gets account status from banking service
func (c *BankingClient) GetAccountStatus(ctx context.Context, accountID string) (map[string]interface{}, error) {
url := fmt.Sprintf("%s/api/v1/banking/accounts/%s", c.baseURL, accountID)
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil {
return nil, err
}
resp, err := c.httpClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("unexpected status: %d", resp.StatusCode)
}
var result map[string]interface{}
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return nil, err
}
return result, nil
}
// CreateTicket creates a support ticket
func (c *BankingClient) CreateTicket(ctx context.Context, subject, details string) (map[string]interface{}, error) {
url := fmt.Sprintf("%s/api/v1/banking/tickets", c.baseURL)
payload := map[string]string{
"subject": subject,
"details": details,
}
jsonData, err := json.Marshal(payload)
if err != nil {
return nil, err
}
req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData))
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", "application/json")
resp, err := c.httpClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusCreated {
return nil, fmt.Errorf("unexpected status: %d", resp.StatusCode)
}
var result map[string]interface{}
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return nil, err
}
return result, nil
}

View File

@@ -0,0 +1,60 @@
package banking
import (
"context"
"github.com/explorer/virtual-banker/backend/tools"
)
// SubmitPaymentTool submits a payment
type SubmitPaymentTool struct{}
// NewSubmitPaymentTool creates a new submit payment tool
func NewSubmitPaymentTool() *SubmitPaymentTool {
return &SubmitPaymentTool{}
}
// Name returns the tool name
func (t *SubmitPaymentTool) Name() string {
return "submit_payment"
}
// Description returns the tool description
func (t *SubmitPaymentTool) Description() string {
return "Submit a payment transaction (requires confirmation)"
}
// Execute executes the tool
func (t *SubmitPaymentTool) Execute(ctx context.Context, params map[string]interface{}) (*tools.ToolResult, error) {
amount, _ := params["amount"].(float64)
method, _ := params["method"].(string)
if amount <= 0 {
return &tools.ToolResult{
Success: false,
Error: "amount must be greater than 0",
}, nil
}
if method == "" {
return &tools.ToolResult{
Success: false,
Error: "payment method is required",
}, nil
}
// TODO: Call backend/banking/payments/ service
// For now, return mock data
return &tools.ToolResult{
Success: true,
Data: map[string]interface{}{
"payment_id": "PAY-11111",
"amount": amount,
"method": method,
"status": "pending_confirmation",
"transaction_id": "TXN-22222",
},
RequiresConfirmation: true, // Payments always require confirmation
}, nil
}

View File

@@ -0,0 +1,62 @@
package banking
import (
"context"
"time"
"github.com/explorer/virtual-banker/backend/tools"
)
// ScheduleAppointmentTool schedules an appointment
type ScheduleAppointmentTool struct{}
// NewScheduleAppointmentTool creates a new schedule appointment tool
func NewScheduleAppointmentTool() *ScheduleAppointmentTool {
return &ScheduleAppointmentTool{}
}
// Name returns the tool name
func (t *ScheduleAppointmentTool) Name() string {
return "schedule_appointment"
}
// Description returns the tool description
func (t *ScheduleAppointmentTool) Description() string {
return "Schedule an appointment with a bank representative"
}
// Execute executes the tool
func (t *ScheduleAppointmentTool) Execute(ctx context.Context, params map[string]interface{}) (*tools.ToolResult, error) {
datetime, _ := params["datetime"].(string)
reason, _ := params["reason"].(string)
if datetime == "" {
return &tools.ToolResult{
Success: false,
Error: "datetime is required",
}, nil
}
// Parse datetime
_, err := time.Parse(time.RFC3339, datetime)
if err != nil {
return &tools.ToolResult{
Success: false,
Error: "invalid datetime format (use RFC3339)",
}, nil
}
// TODO: Call backend/banking/ service to schedule appointment
// For now, return mock data
return &tools.ToolResult{
Success: true,
Data: map[string]interface{}{
"appointment_id": "APT-67890",
"datetime": datetime,
"reason": reason,
"status": "scheduled",
},
RequiresConfirmation: true, // Appointments require confirmation
}, nil
}

89
backend/tools/executor.go Normal file
View File

@@ -0,0 +1,89 @@
package tools
import (
"context"
"fmt"
)
// Executor executes tools
type Executor struct {
registry *Registry
auditLog AuditLogger
}
// NewExecutor creates a new tool executor
func NewExecutor(registry *Registry, auditLog AuditLogger) *Executor {
return &Executor{
registry: registry,
auditLog: auditLog,
}
}
// Execute executes a tool
func (e *Executor) Execute(ctx context.Context, toolName string, params map[string]interface{}, userID, tenantID string) (*ToolResult, error) {
tool, err := e.registry.Get(toolName)
if err != nil {
return nil, err
}
// Log execution attempt
e.auditLog.LogToolExecution(ctx, &ToolExecutionLog{
ToolName: toolName,
UserID: userID,
TenantID: tenantID,
Params: params,
Status: "executing",
})
// Execute tool
result, err := tool.Execute(ctx, params)
if err != nil {
e.auditLog.LogToolExecution(ctx, &ToolExecutionLog{
ToolName: toolName,
UserID: userID,
TenantID: tenantID,
Params: params,
Status: "failed",
Error: err.Error(),
})
return nil, err
}
// Log result
e.auditLog.LogToolExecution(ctx, &ToolExecutionLog{
ToolName: toolName,
UserID: userID,
TenantID: tenantID,
Params: params,
Status: "completed",
Result: result.Data,
})
return result, nil
}
// AuditLogger logs tool executions
type AuditLogger interface {
LogToolExecution(ctx context.Context, log *ToolExecutionLog)
}
// ToolExecutionLog represents a tool execution log entry
type ToolExecutionLog struct {
ToolName string
UserID string
TenantID string
Params map[string]interface{}
Status string
Error string
Result interface{}
}
// MockAuditLogger is a mock audit logger
type MockAuditLogger struct{}
// LogToolExecution logs a tool execution
func (m *MockAuditLogger) LogToolExecution(ctx context.Context, log *ToolExecutionLog) {
// Mock implementation - in production, write to database
fmt.Printf("Tool execution: %s by %s (%s) - %s\n", log.ToolName, log.UserID, log.TenantID, log.Status)
}

73
backend/tools/registry.go Normal file
View File

@@ -0,0 +1,73 @@
package tools
import (
"context"
"fmt"
)
// Tool represents an executable tool
type Tool interface {
Name() string
Description() string
Execute(ctx context.Context, params map[string]interface{}) (*ToolResult, error)
}
// ToolResult represents the result of tool execution
type ToolResult struct {
Success bool
Data interface{}
Error string
RequiresConfirmation bool
}
// Registry manages available tools
type Registry struct {
tools map[string]Tool
}
// NewRegistry creates a new tool registry
func NewRegistry() *Registry {
return &Registry{
tools: make(map[string]Tool),
}
}
// Register registers a tool
func (r *Registry) Register(tool Tool) {
r.tools[tool.Name()] = tool
}
// Get gets a tool by name
func (r *Registry) Get(name string) (Tool, error) {
tool, ok := r.tools[name]
if !ok {
return nil, fmt.Errorf("tool not found: %s", name)
}
return tool, nil
}
// List returns all registered tools
func (r *Registry) List() []Tool {
tools := make([]Tool, 0, len(r.tools))
for _, tool := range r.tools {
tools = append(tools, tool)
}
return tools
}
// GetAllowedTools returns tools allowed for a tenant
func (r *Registry) GetAllowedTools(allowedNames []string) []Tool {
allowedSet := make(map[string]bool)
for _, name := range allowedNames {
allowedSet[name] = true
}
var tools []Tool
for _, tool := range r.tools {
if allowedSet[tool.Name()] {
tools = append(tools, tool)
}
}
return tools
}