Skip to content

Commit 65c8836

Browse files
authored
Merge pull request #232 from micro/claude/mu-killer-feature-yxRo5
Consolidate AI provider to Anthropic and implement FTS5 search
2 parents 7022d4e + c61cb4c commit 65c8836

15 files changed

Lines changed: 221 additions & 1382 deletions

README.md

Lines changed: 4 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -140,59 +140,22 @@ export YOUTUBE_API_KEY=xxx
140140

141141
#### Chat Model
142142

143-
**Ollama (Default)**
144-
145-
By default, Mu uses [Ollama](https://ollama.ai/) for LLM queries. Install and run Ollama locally:
146-
147-
```
148-
# Install Ollama from https://ollama.ai/
149-
# Pull a model (e.g., llama3.2)
150-
ollama pull llama3.2
151-
152-
# Ollama runs on http://localhost:11434 by default
153-
```
154-
155-
Optional environment variables:
156-
```
157-
export MODEL_NAME=llama3.2 # Default model
158-
export MODEL_API_URL=http://localhost:11434 # Ollama API URL
159-
```
160-
161-
**Fanar (Optional)**
162-
163-
Alternatively, use [Fanar](https://fanar.qa/) by setting the API key:
164-
165-
```
166-
export FANAR_API_KEY=xxx
167-
export FANAR_API_URL=https://api.fanar.qa # Optional, this is the default
168-
```
169-
170-
When `FANAR_API_KEY` is set, Mu will use Fanar instead of Ollama.
171-
172-
**Note:** Fanar has a rate limit of 10 requests per minute. Mu enforces this limit automatically.
173-
174-
**Anthropic Claude (Optional)**
175-
176-
You can also use Anthropic's Claude API:
143+
Mu uses Anthropic Claude for all AI features:
177144

178145
```
179146
export ANTHROPIC_API_KEY=xxx
180-
export ANTHROPIC_MODEL=claude-haiku-4.5-20250311 # Optional, this is the default
147+
export ANTHROPIC_MODEL=claude-sonnet-4-20250514 # Optional, this is the default
181148
```
182149

183-
Priority order: Anthropic > Fanar > Ollama
184-
185-
For vector search see this [doc](docs/VECTOR_SEARCH.md)
186-
187150
### Data Storage
188151

189-
By default, Mu stores search index and embeddings in JSON files loaded into memory. For production use with large datasets, enable SQLite storage to reduce memory usage:
152+
By default, Mu stores the search index in JSON files loaded into memory. For production use, enable SQLite with FTS5 full-text search:
190153

191154
```
192155
export MU_USE_SQLITE=1
193156
```
194157

195-
This stores the search index and embeddings in SQLite (`~/.mu/data/index.db`) instead of RAM. Migration from JSON happens automatically on first startup.
158+
This stores the search index in SQLite (`~/.mu/data/index.db`) with FTS5 for fast full-text search. Migration from JSON happens automatically on first startup.
196159

197160
### Run
198161

@@ -221,7 +184,6 @@ Full documentation is available in the [docs](docs/) folder and at `/docs` on an
221184
- [Wallet & Credits](docs/WALLET_AND_CREDITS.md) - Credit system for metered usage
222185
**Reference**
223186
- [Configuration](docs/ENVIRONMENT_VARIABLES.md) - All environment variables
224-
- [Vector Search](docs/VECTOR_SEARCH.md) - Semantic search setup
225187
- [API Reference](docs/API_COVERAGE.md) - REST API endpoints
226188
- [MCP Server](docs/MCP.md) - AI tool integration via MCP
227189
- [Screenshots](docs/SCREENSHOTS.md) - Application screenshots

admin/env.go

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,9 @@ var knownEnvVars = []string{
1717
"MU_DOMAIN",
1818
"MU_USE_SQLITE",
1919
"DATA_DIR",
20-
// LLM providers
20+
// LLM
2121
"ANTHROPIC_API_KEY",
2222
"ANTHROPIC_MODEL",
23-
"FANAR_API_KEY",
24-
"FANAR_API_URL",
25-
"OLLAMA_API_URL",
26-
"MODEL_API_URL",
27-
"MODEL_NAME",
2823
// Search
2924
"BRAVE_API_KEY",
3025
// External APIs

ai/ai.go

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
// Package ai provides LLM integration for the Mu platform.
2-
// It supports multiple providers: Anthropic Claude, Fanar, and Ollama.
1+
// Package ai provides LLM integration for the Mu platform via Anthropic Claude.
32
package ai
43

54
import (
@@ -29,8 +28,6 @@ type History []Message
2928
const (
3029
ProviderDefault = "" // Use configured default
3130
ProviderAnthropic = "anthropic" // Force Anthropic Claude
32-
ProviderFanar = "fanar" // Force Fanar
33-
ProviderOllama = "ollama" // Force Ollama
3431
)
3532

3633
// Prompt represents a request to the LLM

ai/providers.go

Lines changed: 8 additions & 211 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,6 @@ var (
2020
llmSemaphore = semaphore.NewWeighted(5)
2121
llmTimeout = 60 * time.Second
2222

23-
// Rate limiter for Fanar API
24-
fanarRateMu sync.Mutex
25-
fanarLastMinute []time.Time
26-
fanarMaxPerMin = 35
27-
2823
// Anthropic cache stats
2924
cacheStatsMu sync.Mutex
3025
cacheHits int
@@ -62,169 +57,20 @@ func generate(prompt *Prompt) (string, error) {
6257

6358
messages = append(messages, map[string]string{"role": "user", "content": prompt.Question})
6459

65-
// Check for forced provider
66-
if prompt.Provider == ProviderAnthropic {
67-
if key := os.Getenv("ANTHROPIC_API_KEY"); key != "" {
68-
model := prompt.Model
69-
if model == "" {
70-
model = os.Getenv("ANTHROPIC_MODEL")
71-
}
72-
if model == "" {
73-
model = "claude-sonnet-4-20250514"
74-
}
75-
return generateAnthropic(key, model, systemPromptText, messages)
76-
}
77-
return "", fmt.Errorf("anthropic provider requested but ANTHROPIC_API_KEY not set")
78-
}
79-
80-
if prompt.Provider == ProviderFanar {
81-
if key := os.Getenv("FANAR_API_KEY"); key != "" {
82-
url := os.Getenv("FANAR_API_URL")
83-
if url == "" {
84-
url = "https://api.fanar.qa"
85-
}
86-
return generateFanar(url, key, messages, prompt.Priority)
87-
}
88-
return "", fmt.Errorf("fanar provider requested but FANAR_API_KEY not set")
60+
key := os.Getenv("ANTHROPIC_API_KEY")
61+
if key == "" {
62+
return "", fmt.Errorf("ANTHROPIC_API_KEY not set")
8963
}
9064

91-
if prompt.Provider == ProviderOllama {
92-
model := os.Getenv("MODEL_NAME")
93-
if model == "" {
94-
model = "llama3.2"
95-
}
96-
url := os.Getenv("MODEL_API_URL")
97-
if url == "" {
98-
url = "http://localhost:11434"
99-
}
100-
return generateOllama(url, model, messages)
101-
}
102-
103-
// Default provider priority: Anthropic > Fanar > Ollama
104-
// (Anthropic first for quality, Fanar as fallback for Arabic/cultural content)
105-
if key := os.Getenv("ANTHROPIC_API_KEY"); key != "" {
106-
model := os.Getenv("ANTHROPIC_MODEL")
107-
if model == "" {
108-
model = "claude-sonnet-4-20250514"
109-
}
110-
return generateAnthropic(key, model, systemPromptText, messages)
111-
}
112-
113-
if key := os.Getenv("FANAR_API_KEY"); key != "" {
114-
url := os.Getenv("FANAR_API_URL")
115-
if url == "" {
116-
url = "https://api.fanar.qa"
117-
}
118-
return generateFanar(url, key, messages, prompt.Priority)
119-
}
120-
121-
// Default to Ollama
122-
model := os.Getenv("MODEL_NAME")
65+
model := prompt.Model
12366
if model == "" {
124-
model = "llama3.2"
125-
}
126-
url := os.Getenv("MODEL_API_URL")
127-
if url == "" {
128-
url = "http://localhost:11434"
129-
}
130-
return generateOllama(url, model, messages)
131-
}
132-
133-
func generateOllama(apiURL, model string, messages []map[string]string) (string, error) {
134-
app.Log("ai", "[LLM] Using Ollama at %s with model %s", apiURL, model)
135-
136-
req := map[string]interface{}{
137-
"model": model,
138-
"messages": messages,
139-
"stream": false,
67+
model = os.Getenv("ANTHROPIC_MODEL")
14068
}
141-
142-
body, _ := json.Marshal(req)
143-
httpReq, _ := http.NewRequest("POST", apiURL+"/api/chat", bytes.NewReader(body))
144-
httpReq.Header.Set("Content-Type", "application/json")
145-
146-
client := &http.Client{Timeout: llmTimeout}
147-
resp, err := client.Do(httpReq)
148-
if err != nil {
149-
return "", fmt.Errorf("failed to connect to Ollama: %v", err)
150-
}
151-
defer resp.Body.Close()
152-
153-
respBody, _ := io.ReadAll(resp.Body)
154-
155-
var result struct {
156-
Message struct {
157-
Content string `json:"content"`
158-
} `json:"message"`
159-
Error string `json:"error"`
160-
}
161-
json.Unmarshal(respBody, &result)
162-
163-
if result.Error != "" {
164-
return "", fmt.Errorf("ollama error: %s", result.Error)
165-
}
166-
return result.Message.Content, nil
167-
}
168-
169-
func generateFanar(apiURL, apiKey string, messages []map[string]string, priority int) (string, error) {
170-
if !checkFanarRateLimit(priority) {
171-
maxWait := 3
172-
if priority == PriorityHigh {
173-
maxWait = 15
174-
} else if priority == PriorityMedium {
175-
maxWait = 8
176-
}
177-
178-
app.Log("ai", "[LLM] Fanar rate limit reached (priority %d), waiting...", priority)
179-
for i := 0; i < maxWait; i++ {
180-
time.Sleep(time.Second)
181-
if checkFanarRateLimit(priority) {
182-
break
183-
}
184-
if i == maxWait-1 {
185-
return "", fmt.Errorf("fanar rate limit exceeded")
186-
}
187-
}
188-
}
189-
190-
app.Log("ai", "[LLM] Using Fanar at %s", apiURL)
191-
192-
req := map[string]interface{}{
193-
"model": "Fanar",
194-
"messages": messages,
195-
}
196-
body, _ := json.Marshal(req)
197-
198-
httpReq, _ := http.NewRequest("POST", apiURL+"/v1/chat/completions", bytes.NewReader(body))
199-
httpReq.Header.Set("Content-Type", "application/json")
200-
httpReq.Header.Set("Authorization", "Bearer "+apiKey)
201-
202-
client := &http.Client{Timeout: llmTimeout}
203-
resp, err := client.Do(httpReq)
204-
if err != nil {
205-
return "", fmt.Errorf("fanar API request failed: %v", err)
206-
}
207-
defer resp.Body.Close()
208-
209-
respBody, _ := io.ReadAll(resp.Body)
210-
211-
var result struct {
212-
Choices []struct {
213-
Message struct {
214-
Content string `json:"content"`
215-
} `json:"message"`
216-
} `json:"choices"`
217-
Error interface{} `json:"error"`
69+
if model == "" {
70+
model = "claude-sonnet-4-20250514"
21871
}
219-
json.Unmarshal(respBody, &result)
22072

221-
if result.Error != nil {
222-
return "", fmt.Errorf("%v", result.Error)
223-
}
224-
if len(result.Choices) > 0 {
225-
return result.Choices[0].Message.Content, nil
226-
}
227-
return "", nil
73+
return generateAnthropic(key, model, systemPromptText, messages)
22874
}
22975

23076
func generateAnthropic(apiKey, model, systemPrompt string, messages []map[string]string) (string, error) {
@@ -321,55 +167,6 @@ func generateAnthropic(apiKey, model, systemPrompt string, messages []map[string
321167
return content, nil
322168
}
323169

324-
func checkFanarRateLimit(priority int) bool {
325-
fanarRateMu.Lock()
326-
defer fanarRateMu.Unlock()
327-
328-
now := time.Now()
329-
cutoff := now.Add(-time.Minute)
330-
331-
var recent []time.Time
332-
for _, t := range fanarLastMinute {
333-
if t.After(cutoff) {
334-
recent = append(recent, t)
335-
}
336-
}
337-
fanarLastMinute = recent
338-
339-
var maxForPriority int
340-
switch priority {
341-
case PriorityHigh:
342-
maxForPriority = fanarMaxPerMin
343-
case PriorityMedium:
344-
maxForPriority = 25
345-
default:
346-
maxForPriority = 15
347-
}
348-
349-
if len(fanarLastMinute) >= maxForPriority {
350-
return false
351-
}
352-
353-
fanarLastMinute = append(fanarLastMinute, now)
354-
return true
355-
}
356-
357-
// GetFanarRateStatus returns current rate limit status
358-
func GetFanarRateStatus() (used, max int) {
359-
fanarRateMu.Lock()
360-
defer fanarRateMu.Unlock()
361-
362-
now := time.Now()
363-
cutoff := now.Add(-time.Minute)
364-
count := 0
365-
for _, t := range fanarLastMinute {
366-
if t.After(cutoff) {
367-
count++
368-
}
369-
}
370-
return count, fanarMaxPerMin
371-
}
372-
373170
// GetCacheStats returns Anthropic prompt cache statistics
374171
func GetCacheStats() (hits, misses, readTokens, creationTokens int) {
375172
cacheStatsMu.Lock()

0 commit comments

Comments
 (0)