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
7 changes: 4 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
# mu

A personal app platform — blog, chat, news, mail, video and more.
Your personal AI — news, mail, markets, weather, search and more, all through one interface. No ads. No tracking. No algorithm.

## Overview

The current tech ecosystem is totally broken. All the app platform are filled with ads and addictive content. I'm tired of it. You probably are too.
So here's a personal app platform. Blog, chat, news, mail, video and more. No ads, no tracking, no algorithms. Built in the open. It's called Mu for short.
Mu is a personal AI platform. Ask it anything — it checks your mail, looks up prices, searches the web, reads the news, and gives you a personalised answer. Every service is a tool the AI can use on your behalf.

Built in the open. Pay for the tools, not with your attention.

### What's included

Expand Down
20 changes: 16 additions & 4 deletions agent/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,11 @@ import (
"mu/internal/auth"
)

// RunRequest is the input for the synchronous agent endpoint.
// UserContextFunc is set by main.go to provide personalised context
// for the agent's responses. Returns a string with the user's current
// state (unread mail, market prices, etc.) that gets injected into the
// synthesis prompt.
var UserContextFunc func(accountID string) string
type RunRequest struct {
Prompt string `json:"prompt"`
Model string `json:"model"`
Expand Down Expand Up @@ -130,11 +134,19 @@ func RunHandler(w http.ResponseWriter, r *http.Request) {
toolsUsed = append(toolsUsed, ToolUsed{Name: tc.Tool, Status: "ok"})
}

// Step 3: Synthesise
// Step 3: Synthesise with user context.
today := time.Now().UTC().Format("Monday, 2 January 2006 (UTC)")
userCtx := ""
if UserContextFunc != nil {
userCtx = UserContextFunc(acc.ID)
}
synthSystem := "You are Micro, a personal AI assistant. Today is " + today + ". " +
"Answer concisely using the tool results below. Use markdown."
if userCtx != "" {
synthSystem += "\n\nUser context:\n" + userCtx
}
answer, err := ai.Ask(&ai.Prompt{
System: "You are a helpful assistant. Today's date is " + today + ". " +
"Answer using ONLY the tool results below. Use markdown.",
System: synthSystem,
Rag: ragParts,
Question: req.Prompt,
Priority: ai.PriorityHigh,
Expand Down
14 changes: 7 additions & 7 deletions home/home.go
Original file line number Diff line number Diff line change
Expand Up @@ -446,22 +446,22 @@ function fetchW(la,lo){

// ── Cards (always visible) ──
b.WriteString(`<div id="home-cards">`)
b.WriteString(dateHTML)

// Console prompt — inline at the top, before cards. Claude-style
// rounded textarea with send button inside.
// AI prompt — the primary interface. First thing on screen.
if viewerID != "" {
b.WriteString(fmt.Sprintf(`
<div id="console-prompt" style="margin:0 0 16px">
<div id="console-prompt" style="margin:0 0 20px;padding:24px 0 0">
<form id="console-form" style="position:relative">
<textarea id="console-input" placeholder="Search or look up..." maxlength="%d" rows="1" style="width:100%%;padding:10px 40px 10px 12px;border:1px solid #ddd;border-radius:12px;font-size:14px;font-family:inherit;resize:none;box-sizing:border-box;line-height:1.4;overflow:hidden"></textarea>
<button type="submit" style="position:absolute;right:6px;top:50%%;transform:translateY(-50%%);width:28px;height:28px;background:#000;color:#fff;border:none;border-radius:6px;cursor:pointer;display:flex;align-items:center;justify-content:center;font-size:14px;padding:0">&#x2192;</button>
<textarea id="console-input" placeholder="What do you need?" maxlength="%d" rows="1" style="width:100%%;padding:14px 44px 14px 16px;border:1px solid #ddd;border-radius:14px;font-size:16px;font-family:inherit;resize:none;box-sizing:border-box;line-height:1.4;overflow:hidden;background:#fff"></textarea>
<button type="submit" style="position:absolute;right:8px;top:50%%;transform:translateY(-50%%);width:32px;height:32px;background:#000;color:#fff;border:none;border-radius:8px;cursor:pointer;display:flex;align-items:center;justify-content:center;font-size:16px;padding:0">&#x2192;</button>
</form>
<div id="console-response" style="display:none;margin-top:12px;padding:14px;background:#f9f9f9;border-radius:10px"></div>
<div id="console-response" style="display:none;margin-top:14px;padding:16px;background:#f9f9f9;border-radius:12px"></div>
</div>`, stream.MaxContentLength))
b.WriteString(consoleScript)
}

b.WriteString(dateHTML)

// Inline card preferences panel
if viewerAcc != nil {
allCardDefs := []struct{ id, label string }{
Expand Down
17 changes: 14 additions & 3 deletions internal/ai/providers.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,20 @@ var (
cacheReadTokens int
cacheCreationTokens int

// Atlas Cloud config
atlasAPIKey = os.Getenv("ATLAS_API_KEY")
atlasBaseURL = "https://api.atlascloud.ai/v1"
// Atlas Cloud / OpenAI-compatible config.
// Set OPENAI_BASE_URL to use a local model server (Ollama, llama.cpp, etc.)
atlasAPIKey = func() string {
if v := os.Getenv("ATLAS_API_KEY"); v != "" {
return v
}
return os.Getenv("OPENAI_API_KEY")
}()
atlasBaseURL = func() string {
if v := os.Getenv("OPENAI_BASE_URL"); v != "" {
return strings.TrimRight(v, "/")
}
return "https://api.atlascloud.ai/v1"
}()
)

// Atlas Cloud model aliases — used to route requests to Atlas Cloud
Expand Down
18 changes: 18 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,24 @@ func main() {
// load agent
agent.Load()

// Wire user context into the agent — personalises responses.
agent.UserContextFunc = func(accountID string) string {
var parts []string
// Unread mail count.
if unread := mail.GetUnreadCount(accountID); unread > 0 {
parts = append(parts, fmt.Sprintf("- %d unread email(s)", unread))
}
// Wallet balance.
bal := wallet.GetBalance(accountID)
if bal > 0 {
parts = append(parts, fmt.Sprintf("- Wallet: %d credits", bal))
}
if len(parts) == 0 {
return ""
}
return strings.Join(parts, "\n")
}

// Wire digest → blog callbacks (digest publishes as blog post)
digest.PublishBlogPost = func(title, content, author, authorID, tags string) (string, error) {
err := blog.CreatePost(title, content, author, authorID, tags, false)
Expand Down
Loading