Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 7 additions & 4 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,13 @@ REDIS_URI=redis://localhost:6379
AUTH_CACHE_KEY_SECRET=replace-with-strong-random-secret
AUTH_CACHE_TTL_SECONDS=180

# Anthropic Claude API (required)
ANTHROPIC_API_KEY=sk-ant-your-key-here
ANTHROPIC_MODEL=claude-sonnet-4-20250514
ANTHROPIC_SUMMARY_MODEL=claude-haiku-4-5-20251001
# AI Provider - OpenRouter (required)
AI_API_KEY=sk-or-your-key-here
AI_MODEL=anthropic/claude-sonnet-4-5-20250929
AI_SUMMARY_MODEL=anthropic/claude-haiku-4-5-20251001
AI_BASE_URL=https://openrouter.ai/api/v1
AI_APP_NAME=vultisig-agent
AI_APP_URL=https://vultisig.com

# Conversation context window
CONTEXT_WINDOW_SIZE=20
Expand Down
16 changes: 10 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# agent-backend

AI Chat Agent backend service for Vultisig mobile apps. This service handles natural language conversations using Anthropic Claude and coordinates with existing Vultisig plugins (app-recurring, feeplugin) via the verifier.
AI Chat Agent backend service for Vultisig mobile apps. This service handles natural language conversations using LLMs via OpenRouter and coordinates with existing Vultisig plugins (app-recurring, feeplugin) via the verifier.

## Prerequisites

Expand All @@ -19,8 +19,12 @@ AI Chat Agent backend service for Vultisig mobile apps. This service handles nat
| `REDIS_URI` | Yes | - | Redis connection URI |
| `AUTH_CACHE_KEY_SECRET` | Yes | - | HMAC secret for auth cache key derivation |
| `AUTH_CACHE_TTL_SECONDS` | No | `180` | Auth cache TTL (seconds) |
| `ANTHROPIC_API_KEY` | Yes | - | Anthropic Claude API key |
| `ANTHROPIC_MODEL` | No | `claude-sonnet-4-20250514` | Claude model to use |
| `AI_API_KEY` | Yes | - | OpenRouter API key |
| `AI_MODEL` | No | `anthropic/claude-sonnet-4-5-20250929` | Model to use (OpenRouter format) |
| `AI_SUMMARY_MODEL` | No | `anthropic/claude-haiku-4-5-20251001` | Model for conversation summarization |
| `AI_BASE_URL` | No | `https://openrouter.ai/api/v1` | AI provider base URL |
| `AI_APP_NAME` | No | `vultisig-agent` | App name sent to OpenRouter |
| `AI_APP_URL` | No | - | App URL sent to OpenRouter |
| `VERIFIER_URL` | Yes | - | Verifier service base URL |
| `LOG_FORMAT` | No | `json` | Log format (`json` or `text`) |

Expand All @@ -32,7 +36,7 @@ AI Chat Agent backend service for Vultisig mobile apps. This service handles nat
export DATABASE_DSN="postgres://user:pass@localhost:5432/agent?sslmode=disable"
export REDIS_URI="redis://localhost:6379"
export AUTH_CACHE_KEY_SECRET="replace-with-strong-random-secret"
export ANTHROPIC_API_KEY="sk-ant-..."
export AI_API_KEY="sk-or-v1-..."
export VERIFIER_URL="http://localhost:8080"
```

Expand Down Expand Up @@ -63,7 +67,7 @@ Run with Docker:
docker run -p 8080:8080 \
-e DATABASE_DSN="postgres://..." \
-e REDIS_URI="redis://..." \
-e ANTHROPIC_API_KEY="sk-ant-..." \
-e AI_API_KEY="sk-or-v1-..." \
-e VERIFIER_URL="http://verifier:8080" \
agent-backend:latest
```
Expand Down Expand Up @@ -117,7 +121,7 @@ internal/
service/ # Business logic layer
storage/postgres/ # PostgreSQL repositories + migrations
cache/redis/ # Redis caching
ai/anthropic/ # Anthropic Claude integration
ai/ # AI client (OpenRouter-compatible)
config/ # Configuration loading
types/ # Shared types
```
8 changes: 4 additions & 4 deletions cmd/server/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import (
"github.com/labstack/echo/v4/middleware"
"github.com/sirupsen/logrus"

"github.com/vultisig/agent-backend/internal/ai/anthropic"
"github.com/vultisig/agent-backend/internal/ai"
"github.com/vultisig/agent-backend/internal/api"
"github.com/vultisig/agent-backend/internal/cache/redis"
"github.com/vultisig/agent-backend/internal/config"
Expand Down Expand Up @@ -58,8 +58,8 @@ func main() {
}
defer redisClient.Close()

// Initialize Anthropic client
anthropicClient := anthropic.NewClient(cfg.Anthropic.APIKey, cfg.Anthropic.Model)
// Initialize AI client
aiClient := ai.NewClient(cfg.AI.APIKey, cfg.AI.Model, cfg.AI.BaseURL, cfg.AI.AppName, cfg.AI.AppURL)

// Initialize plugin service (skills fetched dynamically on demand)
pluginService := plugin.NewService(cfg.Verifier.URL, redisClient, logger)
Expand All @@ -80,7 +80,7 @@ func main() {
}

// Initialize agent service
agentService := agent.NewAgentService(anthropicClient, msgRepo, convRepo, memRepo, redisClient, verifierClient, pluginService, swapTxBuilder, logger, cfg.Anthropic.SummaryModel, cfg.Context)
agentService := agent.NewAgentService(aiClient, msgRepo, convRepo, memRepo, redisClient, verifierClient, pluginService, swapTxBuilder, logger, cfg.AI.SummaryModel, cfg.Context)

// Initialize API server
server := api.NewServer(
Expand Down
24 changes: 17 additions & 7 deletions deploy/01_server.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -31,17 +31,27 @@ spec:
value: "80"
- name: LOG_FORMAT
value: "json"
# --- Anthropic config (from ConfigMap) ---
- name: ANTHROPIC_MODEL
# --- AI config (from ConfigMap) ---
- name: AI_MODEL
valueFrom:
configMapKeyRef:
name: agent
key: anthropic-model
- name: ANTHROPIC_SUMMARY_MODEL
key: ai-model
- name: AI_SUMMARY_MODEL
valueFrom:
configMapKeyRef:
name: agent
key: anthropic-summary-model
key: ai-summary-model
- name: AI_BASE_URL
valueFrom:
configMapKeyRef:
name: agent
key: ai-base-url
- name: AI_APP_NAME
valueFrom:
configMapKeyRef:
name: agent
key: ai-app-name
# --- Context window config (from ConfigMap) ---
- name: CONTEXT_WINDOW_SIZE
valueFrom:
Expand Down Expand Up @@ -75,10 +85,10 @@ spec:
secretKeyRef:
name: redis
key: uri
- name: ANTHROPIC_API_KEY
- name: AI_API_KEY
valueFrom:
secretKeyRef:
name: anthropic
name: ai-provider
key: api-key
- name: AUTH_CACHE_KEY_SECRET
valueFrom:
Expand Down
6 changes: 4 additions & 2 deletions deploy/dev/01_agent.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@ kind: ConfigMap
metadata:
name: agent
data:
anthropic-model: "claude-sonnet-4-20250514"
anthropic-summary-model: "claude-haiku-4-5-20251001"
ai-model: "anthropic/claude-sonnet-4-5-20250929"
ai-summary-model: "anthropic/claude-haiku-4-5-20251001"
ai-base-url: "https://openrouter.ai/api/v1"
ai-app-name: "vultisig-agent"
context-window-size: "20"
context-summarize-trigger: "30"
context-summary-max-tokens: "512"
6 changes: 4 additions & 2 deletions deploy/prod/01_agent.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@ kind: ConfigMap
metadata:
name: agent
data:
anthropic-model: "claude-sonnet-4-20250514"
anthropic-summary-model: "claude-haiku-4-5-20251001"
ai-model: "anthropic/claude-sonnet-4-5-20250929"
ai-summary-model: "anthropic/claude-haiku-4-5-20251001"
ai-base-url: "https://openrouter.ai/api/v1"
ai-app-name: "vultisig-agent"
context-window-size: "20"
context-summarize-trigger: "30"
context-summary-max-tokens: "512"
44 changes: 28 additions & 16 deletions internal/ai/anthropic/client.go → internal/ai/client.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package anthropic
package ai

import (
"bufio"
Expand All @@ -13,19 +13,19 @@ import (
)

const (
defaultBaseURL = "https://api.anthropic.com/v1"
defaultMaxTokens = 4096
apiVersion = "2023-06-01"
maxRetries = 3
baseRetryDelay = 1 * time.Second
)

// Client is an Anthropic Claude API client.
// Client is an OpenRouter-compatible AI API client.
type Client struct {
apiKey string
model string
httpClient *http.Client
baseURL string
appName string
appURL string
}

// Message represents a simple conversation message with string content.
Expand Down Expand Up @@ -55,14 +55,14 @@ type ToolResultBlock struct {
IsError bool `json:"is_error,omitempty"`
}

// Tool represents a tool that Claude can use.
// Tool represents a tool that the model can use.
type Tool struct {
Name string `json:"name"`
Description string `json:"description"`
InputSchema any `json:"input_schema"`
}

// ToolChoice specifies how Claude should use tools.
// ToolChoice specifies how the model should use tools.
type ToolChoice struct {
Type string `json:"type"` // "auto", "any", or "tool"
Name string `json:"name,omitempty"` // Required when type is "tool"
Expand Down Expand Up @@ -106,30 +106,32 @@ type Usage struct {
OutputTokens int `json:"output_tokens"`
}

// APIError represents an error from the Anthropic API.
// APIError represents an error from the AI API.
type APIError struct {
Type string `json:"type"`
Message string `json:"message"`
StatusCode int `json:"-"`
}

func (e *APIError) Error() string {
return fmt.Sprintf("anthropic: %s: %s", e.Type, e.Message)
return fmt.Sprintf("ai: %s: %s", e.Type, e.Message)
}

// NewClient creates a new Anthropic client.
func NewClient(apiKey, model string) *Client {
// NewClient creates a new AI client.
func NewClient(apiKey, model, baseURL, appName, appURL string) *Client {
return &Client{
apiKey: apiKey,
model: model,
baseURL: defaultBaseURL,
baseURL: baseURL,
appName: appName,
appURL: appURL,
httpClient: &http.Client{
Timeout: 90 * time.Second,
},
}
}

// SendMessage sends a message to Claude and returns the response.
// SendMessage sends a message to the model and returns the response.
func (c *Client) SendMessage(ctx context.Context, req *Request) (*Response, error) {
if req.Model == "" {
req.Model = c.model
Expand Down Expand Up @@ -175,8 +177,13 @@ func (c *Client) doRequest(ctx context.Context, body []byte) (*Response, error)
}

httpReq.Header.Set("Content-Type", "application/json")
httpReq.Header.Set("x-api-key", c.apiKey)
httpReq.Header.Set("anthropic-version", apiVersion)
httpReq.Header.Set("Authorization", "Bearer "+c.apiKey)
if c.appName != "" {
httpReq.Header.Set("X-Title", c.appName)
}
if c.appURL != "" {
httpReq.Header.Set("HTTP-Referer", c.appURL)
}

resp, err := c.httpClient.Do(httpReq)
if err != nil {
Expand Down Expand Up @@ -273,8 +280,13 @@ func (c *Client) SendMessageStream(ctx context.Context, req *Request, callback S
}

httpReq.Header.Set("Content-Type", "application/json")
httpReq.Header.Set("x-api-key", c.apiKey)
httpReq.Header.Set("anthropic-version", apiVersion)
httpReq.Header.Set("Authorization", "Bearer "+c.apiKey)
if c.appName != "" {
httpReq.Header.Set("X-Title", c.appName)
}
if c.appURL != "" {
httpReq.Header.Set("HTTP-Referer", c.appURL)
}

resp, err := c.httpClient.Do(httpReq)
if err != nil {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package anthropic
package ai

import (
"strings"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package anthropic
package ai

import (
"testing"
Expand Down
15 changes: 9 additions & 6 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ type Config struct {
Database DatabaseConfig
Redis RedisConfig
AuthCache AuthCacheConfig
Anthropic AnthropicConfig
AI AIConfig
Context ContextConfig
Verifier VerifierConfig
MCP MCPConfig
Expand Down Expand Up @@ -39,11 +39,14 @@ type AuthCacheConfig struct {
TTLSeconds int `envconfig:"AUTH_CACHE_TTL_SECONDS" default:"180"`
}

// AnthropicConfig holds Anthropic Claude API configuration.
type AnthropicConfig struct {
APIKey string `envconfig:"ANTHROPIC_API_KEY" required:"true"`
Model string `envconfig:"ANTHROPIC_MODEL" default:"claude-sonnet-4-20250514"`
SummaryModel string `envconfig:"ANTHROPIC_SUMMARY_MODEL" default:"claude-haiku-4-5-20251001"`
// AIConfig holds AI provider configuration (OpenRouter-compatible).
type AIConfig struct {
APIKey string `envconfig:"AI_API_KEY" required:"true"`
Model string `envconfig:"AI_MODEL" default:"anthropic/claude-sonnet-4-5-20250929"`
SummaryModel string `envconfig:"AI_SUMMARY_MODEL" default:"anthropic/claude-haiku-4-5-20251001"`
BaseURL string `envconfig:"AI_BASE_URL" default:"https://openrouter.ai/api/v1"`
AppName string `envconfig:"AI_APP_NAME" default:"vultisig-agent"`
AppURL string `envconfig:"AI_APP_URL" default:""`
}

// TODO: Add WhisperConfig for OpenAI Whisper voice transcription support.
Expand Down
Loading