# Project export: Mental Galaxy

This document was generated by HackStack to give an AI agent context about a hackathon project. Sections are labeled with their provenance; content marked as truncated was cut to keep this document small.

## Project metadata

- Hackathon: UC Berkeley AI Hackathon 2026
- Tagline: Create a dynamic visual mind map of your life, tasks, or projects with ease by speaking or typing into the Mental Galaxy web application.
- Devpost: https://devpost.com/software/mental-galaxy
- GitHub: https://github.com/gargi-ramacharan/mentalgalaxy
- Video: https://www.youtube.com/embed/MVFCKyGMNwA?enablejsapi=1&hl=en_US&rel=0&start=&version=3&wmode=transparent
- Team: 4 GitHub contributor(s) — Medha Rakesh (21 commits), Linda Chang (20 commits), Claude Sonnet 4.6 (11 commits), gargi-ramacharan (3 commits)

## Devpost submission (written by the team)

### Inspiration

Your head is the loudest exactly when you have the least amount of time to deal with it: finals week, a hackathon, a Sunday night with a flight you haven't packed for. It all comes out as one run-on thought: "Stressed about tests, project due Friday, interview Saturday, flight Sunday -- and I'm not packed." Every note app makes you stop and organize that yourself, which is the last thing you want to do mid-spiral. We wanted the opposite: just talk, and watch it sort itself out into something you can actually act on, not just another empty doc staring back at you.

### What it does

Mental Galaxy is a voice-first thought-mapping app. You hold the mic and talk through your day. Your words stream in live, each thought gets classified into a task, emotion, or idea, and drops onto an animated, force-directed galaxy of bubbles with connections drawn between related thoughts. From there, it goes further than a map: Guidance: Tap any bubble and ask, "What should I do?" An agent pulls from your past sessions and suggests one concrete, grounded next step. It only runs when asked, so it never nags. Action: Hit "Execute" on a task bubble, and specialized agents take over -- adding the event to your calendar or drafting an email. The bubble turns green when it's done.

### How we built it

We split the work three ways -- one person on the voice pipeline, one on the canvas, one on agents/infra -- and converged at three milestones (the map, the guidance, the action) so we always had a demoable product instead of betting everything on the final hour. Frontend & Voice: React + D3 for the force-directed canvas. Deepgram Nova-2 streams speech-to-text, piping raw PCM audio over a WebSocket using the AudioWorklet API for low latency. Reasoning Orchestration: FastAPI orchestrates the pipeline. Claude (Anthropic) classifies each thought and finds connections, returning the structured JSON that drives the galaxy. Memory: Redis + RedisVL for vector search over past sessions. This ensures the agent actually knows what you said last week, making suggestions feel grounded instead of generic. Agents: Fetch.ai uAgents (registered on Agentverse) route each bubble to the right specialized agent. Google Calendar and Gmail APIs act as the agents' actual hands. Observability: Arize logs every Claude classification decision to watch the model's behavior in real-time, while Sentry monitors the stack for errors.

### Challenges we ran into

Getting an LLM to emit reliable, structured JSON under the pressure of messy, real-speech input took massive prompt iteration. For example, getting the model to correctly file "violin" and "cross country" under an existing "hobbies" bubble, rather than inventing three new bubbles, took significant work. We fixed this by reframing existing sections as "containers" rather than "peers," which completely changed how the model reasoned about placement. Other hurdles included handling the backpressure of streaming audio over websockets, maintaining D3 force-simulation performance as the galaxy got crowded, and wiring up OAuth under serious time pressure.

### What we learned

Reliable LLM outputs require constant refinement, and observability in a multi-stage AI pipeline isn't a nice-to-have. When something can break at Deepgram, Claude, Redis, or an agent, Arize and Sentry were the only reasons we could find the break fast. But the biggest lesson wasn't technical. The best demo moment wasn't any specific feature we planned; it was the silence right after you stop talking, watching the galaxy automatically build and organize itself.

### What's next

Richer connections: Adding causal and temporal links between bubbles, not just "related." More execution agents: Expanding beyond calendar and email integrations. Multiplayer: Shared galaxies so a team can map out a project together out loud. Mobile App: So you can talk through your commute and watch your thoughts organize by the time you get home.

## README (from the GitHub repository)

# Mental Galaxy 🌌

Voice-first thought mapping. You talk through your day — your stresses, your tasks, your half-formed ideas — and watch them organize themselves into a living constellation. Ask any bubble for guidance and an agent pulls from everything you've said before to suggest a real next step. When you're ready, it can go execute the tasks for you.

Built for UC Berkeley AI Hackathon 2026.

---

## The three milestones

This repo is structured so you can ship at any of three stopping points. Each one is a complete, demoable product on its own.

**Milestone 1 — The Map.** Speak → live transcription → thoughts classified into task / emotion / idea → animated bubble constellation with connections drawn between related thoughts. Sessions persist. This is the wow moment and it stands alone.

**Milestone 2 — The Guidance.** Tap any bubble and ask "what should I do?" An agent pulls semantically related moments from your past sessions and suggests a concrete, grounded next step. Only runs when you ask — it never nags.

**Milestone 3 — The Action.** Task bubbles get an Execute button. Specialized agents add events to your calendar or draft emails. The bubble turns green when done. This is last because it's riskiest; everything above it already wins.

---

## Architecture

```
                    ┌─────────────┐
   speak ──────────▶│  Deepgram   │  streaming speech-to-text
                    └──────┬──────┘
                           │ transcript
                    ┌──────▼──────┐
                    │   Claude    │  classify → {task|emotion|idea}
                    │ (Anthropic) │  + find connections
                    └──────┬──────┘
                           │ JSON nodes
        ┌──────────────────┼──────────────────┐
        │                  │                  │
  ┌─────▼─────┐     ┌──────▼──────┐    ┌──────▼──────┐
  │  Canvas   │     │    Redis    │    │   Fetch.ai  │
  │ (D3 bubble│     │ vector mem  │    │   agents    │
  │  galaxy)  │     │  + search   │    │ (uAgents)   │
  └───────────┘     └─────────────┘    └──────┬──────┘
                                              │
                              ┌───────────────┼───────────────┐
                        ┌─────▼─────┐  ┌──────▼─────┐  ┌──────▼─────┐
                        │  Insight  │  │  Calendar  │  │   Email    │
                        │   Agent   │  │   Agent    │  │   Agent    │
                        │ (M2)      │  │ (M3)       │  │ (M3)       │
                        └───────────┘  └─────┬──────┘  └─────┬──────┘
                                       Google Cal API   Gmail API

  Arize logs every Claude classification · Sentry watches everything
```

## Tech stack & who does what

| Layer | Tool | Role | Milestone |
|---|---|---|---|
| Voice | **Deepgram** | streaming STT, words appear as you speak | 1 |
| Reasoning | **Claude (Anthropic)** | classify thoughts, find connections, suggestions | 1, 2 |
| Canvas | **React + D3** | force-directed bubble galaxy | 1 |
| Memory | **Redis** | vector search over past sessions, agent memory | 2 |
| Orchestration | **Fetch.ai (uAgents)** | route bubbles to the right agent, run in parallel | 2, 3 |
| Execution | **Google Calendar + Gmail** | the agents' actual hands | 3 |
| Observability | **Arize** | dashboard of every classification decision | 1 |
| Reliability | **Sentry** | error monitoring across the stack | 1 |

## Repo layout

```
thought-galaxy/
├── backend/          FastAPI — orchestrates Deepgram, Claude, Redis, agents
│   ├── app/
│   │   ├── main.py             entry + WebSocket for live transcription
│   │   ├── deepgram_stream.py  voice → text
│   │   ├── classify.py         Claude: transcript → bubble JSON
│   │   ├── suggest.py          Claude + Redis: bubble → suggestion  (M2)
│   │   ├── memory.py           Redis vector store + search          (M2)
│   │   ├── observability.py    Arize + Sentry setup
│   │   └── schemas.py          shared data shapes
│   └── requirements.txt
├── agents/           Fetch.ai uAgents
│   ├── insight_agent.py        suggestions from past context        (M2)
│   ├── calendar_agent.py       Google Calendar                      (M3)
│   ├── email_agent.py          Gmail                                (M3)
│   └── requirements.txt
├── frontend/         React + D3 bubble canvas
│   └── src/
│       ├── App.jsx
│       ├── Galaxy.jsx          the D3 force-directed map
│       ├── useRecorder.js      mic capture → backend WebSocket
│       └── api.js
└── .env.example      every key you need, in one place
```

## Quick start

```bash
# 1. Backend
cd backend
python -m venv venv && source venv/bin/activate
pip install -r requirements.txt
cp ../.env.example .env   # fill in your keys
uvicorn app.main:app --reload

# 2. Agents (separate terminal, Milestone 2+)
cd agents
pip install -r requirements.txt
python insight_agent.py

# 3. Frontend (separate terminal)
cd frontend
npm install
npm run dev
```

## Division of labor (3 people)

- **Person A — Pipeline:** `deepgram_stream.py`, `classify.py`, the WebSocket in `main.py`. Owns voice→nodes.
- **Person B — Canvas:** all of `frontend/`. Owns the visual wow.
- **Person C — Agents + infra:** `agents/`, `memory.py`, `observability.py`. Owns Redis, Fetch.ai, Arize, Sentry.

A and B merge at Milestone 1. Then everyone converges on Milestone 2 before touching Milestone 3.


## Detected evidence (automated analysis)

Indexed codebase: 29 recognized source files, 296 KB.
- Anthropic (technology) — detected in the code
- CSS (language) — detected in the code
- FastAPI (technology) — detected in the code
- HTML (language) — detected in the code
- JavaScript (language) — detected in the code
- Python (language) — detected in the code
- React (technology) — detected in the code
- Redis (technology) — detected in the code
- AI coding agent: Claude Code — evidence: commit authorship or trailers

## Codebase structure (from repository index)

### Files (37 of 37)

```
.gitignore
.vscode/settings.json
.vscode/state.json
agents/__init__.py
agents/calendar_agent.py
agents/email_agent.py
agents/insight_agent.py
agents/requirements.txt
agents/test_calendar.py
backend/.gitignore
backend/app/__init__.py
backend/app/agent_bridge.py
backend/app/classify.py
backend/app/deepgram_stream.py
backend/app/extract.py
backend/app/llm.py
backend/app/main.py
backend/app/memory.py
backend/app/observability.py
backend/app/schemas.py
backend/app/smoke_test.py
backend/app/suggest.py
backend/requirements.txt
BUILD_PLAN.md
frontend/index.html
frontend/index.html.bak
frontend/package.json
frontend/src/api.js
frontend/src/App.jsx
frontend/src/Galaxy.jsx
frontend/src/index.html
frontend/src/main.jsx
frontend/src/mindmap.html
frontend/src/styles.css
frontend/src/useRecorder.js
frontend/vite.config.js
README.md
```

### Dependencies

- agents/requirements.txt: anthropic@==0.39.0, google-api-python-client@==2.143.0, google-auth@==2.34.0, google-auth-oauthlib@==1.2.1, uagents@==0.18.1
- backend/requirements.txt: anthropic@==0.39.0, arize@==7.19.0, deepgram-sdk@==2.12.0, fastapi@==0.115.0, google-api-python-client@==2.197.0, google-auth-httplib2@==0.4.0, google-auth-oauthlib@==1.4.0, httpx@==0.27.2, numpy@==1.26.4, pandas@==2.2.2, pydantic@==2.9.2, python-dotenv@==1.0.1, redis@==5.0.8, redisvl@==0.3.9, sentence-transformers@==3.3.1, sentry-sdk@==2.14.0, uvicorn[standard]@==0.30.6, websockets@==11.0.3
- frontend/package.json: @vitejs/plugin-react@^4.3.1, d3@^7.9.0, react@^18.3.1, react-dom@^18.3.1, vite@^5.4.2

### Recent commits (newest first)

- Rename project from 'Thought Galaxy' to 'Mental Galaxy'
- Fix duplicate sub-bubbles when filing into template containers
- Subtopic reconciliation: reuse existing sub-bubbles instead of duplicating
- subtopics redefining
- Merge branch 'main' of https://github.com/gargi-ramacharan/mentalgalaxy
- Fix Google Calendar OAuth (PKCE + missing packages)
- Merge branch 'main' of https://github.com/gargi-ramacharan/mentalgalaxy
- backend version discrepencies
- Restore graph context to voice path (lost in Linda's cleanup)
- resolved merge conflict
- Merge branch 'main' of https://github.com/gargi-ramacharan/mentalgalaxy into main
- merge: deepgram fix + asyncio.to_thread + linda/gargi features
- Fix /suggest for multi-thought sessions (Linda's Bug #1 + #2 polish)
- Fix /suggest alias lookup for renamed bubbles
- Fix M3 agent routing + fold subtopics into M2 embeddings
- delete
- final
- edited spacing
- cleaned everything up a bit
- Merge branch 'main' of https://github.com/gargi-ramacharan/thoughtgalaxy

## Key source files (fetched from GitHub, selected and truncated for size)

### BUILD_PLAN.md

```markdown
# Build Plan — Thought Galaxy
**Team:** Medha (A · pipeline), Linda (B · galaxy UI), Gargi (C · infra + agents)
**Deadline:** Sunday 11am submission, judging 1-3pm
**Current time:** ~6pm Saturday. ~15 hours left, ~11 productive.

---

## RIGHT NOW — 6pm to 7pm · Everyone: get running

Do these in parallel. Don't help each other until your own machine is working.

**Medha (A)**
```bash
cd backend
python -m venv venv && source venv/bin/activate
pip install -r requirements.txt
cp ../.env.example .env   # fill in DEEPGRAM_API_KEY + ANTHROPIC_API_KEY now
uvicorn app.main:app --reload
```
→ hit localhost:8000/health → must return {"ok":true} before moving on

**Linda (B)**
```bash
cd frontend
npm install
npm run dev
```
→ open localhost:5173 AND your mindmap.html side by side
→ your job all night is making the React app look + feel like your HTML

**Gargi (C)**
- Create Redis Cloud free account → get connection string → paste as REDIS_URL in .env
- Get Sentry DSN + Arize keys → paste into .env
- Run: `python -c "from dotenv import load_dotenv; load_dotenv(); from app.memory import ensure_index; ensure_index(); print('Redis OK')"`

---

## 7pm to 9pm · Build in parallel, no merging yet

### Medha (A) — Deepgram → terminal
One job: speak into mic, see classified JSON nodes printed in terminal.
- Test `deepgram_stream.py` directly first — just get words appearing
- Then add the Claude classify call — paste a transcript, get nodes back
- Do NOT touch the WebSocket yet. Terminal only.

```bash
# quick test once backend is running:
curl -X POST http://localhost:8000/classify \
  -H "Content-Type: application/json" \
  -d '{"transcript": "I have a CS midterm friday and im stressed, also need to email my professor about an extension"}'
```
→ should return a session with 2-3 nodes. If it does, you're done for this block.

### Linda (B) — Port blob bubbles into Galaxy.jsx
Reference your mindmap.html the whole time. Specifically port:
- `blobPath()` function → add to Galaxy.jsx
- `drawNode()` with the glow + sparkle dots → replace the basic D3 circles
- Your color system: the organic palette, opacity, shadow blur
- The gentle `alphaDecay` drift so it feels alive

Don't worry about cluster zoom yet. Just get blobs rendering where circles are.

### Gargi (C) — Wire Redis to actually save sessions
Right now `main.py` saves sessions in a try/except that silently fails. Make it real:
- Confirm `ensure_index()` passes
- Call `save_session()` with a fake session object and confirm the key appears in Redis Cloud dashboard
- Then confirm `get_session()` retrieves it back
- Add `OPENAI_API_KEY` to .env (needed for real embeddings in memory.py — free tier is fine)

---

## 9pm · SYNC #1 (15 minutes, everyone stops)

Show each other:
- Medha: curl /classify returns clean nodes ✓
- Linda: blobs visible on canvas in React ✓
- Gargi: Redis save/retrieve confirmed ✓

If all three work → wire Medha into Linda's canvas now.
If any are broken → all three debug together before movin
[truncated — 5577 more characters]
```

### agents/requirements.txt

```
uagents==0.18.1
anthropic==0.39.0
google-auth==2.34.0
google-auth-oauthlib==1.2.1
google-api-python-client==2.143.0

```

### frontend/package.json

```
{
  "name": "thought-galaxy-frontend",
  "private": true,
  "version": "0.1.0",
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "preview": "vite preview"
  },
  "dependencies": {
    "d3": "^7.9.0",
    "react": "^18.3.1",
    "react-dom": "^18.3.1"
  },
  "devDependencies": {
    "@vitejs/plugin-react": "^4.3.1",
    "vite": "^5.4.2"
  }
}

```

### backend/requirements.txt

```
fastapi==0.115.0
uvicorn[standard]==0.30.6
anthropic==0.39.0
httpx==0.27.2  # pinned: anthropic 0.39.0 breaks with httpx>=0.28 (proxies arg removed)
deepgram-sdk==2.12.0  # pinned: app/deepgram_stream.py uses the v2 API (Deepgram class); v3 removes it
websockets==11.0.3  # pinned: deepgram-sdk 2.x calls websockets.connect(extra_headers=...), removed in websockets>=14
redis==5.0.8
numpy==1.26.4
pydantic==2.9.2
sentry-sdk==2.14.0
# Arize (optional, fails soft if absent)
arize==7.19.0
pandas==2.2.2
python-dotenv==1.0.1
google-auth-oauthlib==1.4.0
google-auth-httplib2==0.4.0
google-api-python-client==2.197.0
redisvl==0.3.9
sentence-transformers==3.3.1

```

### frontend/src/main.jsx

```javascript
import React from "react";
import { createRoot } from "react-dom/client";
import App from "./App";
createRoot(document.getElementById("root")).render(<App />);

```

### frontend/src/App.jsx

```javascript
import { useState, useCallback } from "react";
import Galaxy from "./Galaxy";
import { useRecorder } from "./useRecorder";
import { suggest, execute } from "./api";
import "./styles.css";

/**
 * App — the full Thought Galaxy experience.
 *
 *  M1: hold to talk → live transcript → bubbles bloom into a galaxy
 *  M2: tap a bubble → a guidance card slides in, grounded in past sessions
 *  M3: a task bubble's card shows Execute → an agent runs it
 */
export default function App() {
  const [nodes, setNodes] = useState([]);
  const [partial, setPartial] = useState("");
  const [sessionId, setSessionId] = useState(null);
  const [card, setCard] = useState(null); // {node, suggestion?, loading?}

  const onNodes = useCallback((incoming) => {
    setNodes((prev) => {
      const byId = new Map(prev.map((n) => [n.id, n]));
      incoming.forEach((n) => byId.set(n.id, { ...byId.get(n.id), ...n }));
      return [...byId.values()];
    });
  }, []);

  const { recording, start, stop } = useRecorder({
    onPartial: setPartial,
    onNodes,
  });

  // M2 — tap a bubble for guidance
  const onTap = useCallback(
    async (node) => {
      setCard({ node, loading: true });
      try {
        const s = await suggest(node.id, sessionId);
        setCard({ node, suggestion: s.text, drawnFrom: s.drawn_from });
      } catch {
        setCard({ node, suggestion: "Couldn't reach the guidance agent." });
      }
    },
    [sessionId]
  );

  // M3 — run an agent on a task bubble
  const onExecute = useCallback(
    async (node) => {
      setCard((c) => ({ ...c, executing: true }));
      const res = await execute(node.id, sessionId);
      setCard((c) => ({ ...c, executing: false, result: res }));
    },
    [sessionId]
  );

  return (
    <div className="app">
      <header>
        <h1>Thought Galaxy</h1>
        <p className="tag">say what's on your mind — watch it organize itself</p>
      </header>

      <div className="canvas">
        <Galaxy nodes={nodes} onTap={onTap} />
        {nodes.length === 0 && !recording && (
          <div className="empty">Hold the orb and talk through your day.</div>
        )}
      </div>

      {partial && recording && <div className="partial">{partial}</div>}

      <button
        className={`orb ${recording ? "live" : ""}`}
        onMouseDown={start}
        onMouseUp={stop}
        onTouchStart={start}
        onTouchEnd={stop}
      >
        {recording ? "listening…" : "hold to talk"}
      </button>

      {card && (
        <div className="card" onClick={(e) => e.stopPropagation()}>
          <button className="close" onClick={() => setCard(null)}>×</button>
          <div className={`chip ${card.node.type}`}>{card.node.type}</div>
          <h3>{card.node.text}</h3>

          {card.loading && <p className="muted">thinking it through…</p>}

          {card.suggestion && <p className="suggestion">{card.suggestion}</p>}

          {card.drawnFrom?.length > 0 && (
            <p className="drawn">drew on: {card.drawnFrom.join(" · ")}</p>
          )}

          {/* M3 — only task bubbles can be executed */}
          {card.node.type === "task" && !card.result && (
            <button
              className="exec"
              disabled={card.executing}
              onClick={() => onExecute(card.node)}
            >
              {card.executing ? "running…" : "let an agent handle it"}
            </button>
          )}

          {card.result && (
            <p className={card.result.status === "done" ? "ok" : "fail"}>
              {card.result.status === "done"
                ? "done — check your calendar / drafts"
                : "couldn't complete that one"}
            </p>
          )}
        </div>
      )}
    </div>
  );
}

```

### backend/app/main.py

```python
"""Thought Galaxy backend — FastAPI.

Endpoints
  WS   /ws/transcribe   live mic audio in → transcript + nodes out   (M1)
  POST /classify        transcript → nodes (non-streaming fallback)  (M1)
  POST /suggest         tap a bubble, get one grounded next step     (M2)
  GET  /search          semantic search over past thoughts           (M2)
  POST /execute         run an agent on a task node                  (M3)

The WebSocket is the heart of the live demo. The REST routes make it easy to
test each layer in isolation (and give you a fallback path if the socket
misbehaves on stage).
"""
import os
import uuid
import json
import datetime
import asyncio
from fastapi import FastAPI, WebSocket, WebSocketDisconnect
from fastapi.responses import RedirectResponse
from fastapi.middleware.cors import CORSMiddleware
from dotenv import load_dotenv
load_dotenv()

BACKEND_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
GOOGLE_CREDS_PATH = os.environ.get(
    "GOOGLE_CREDENTIALS_PATH",
    os.path.join(BACKEND_DIR, "google_credentials.json"),
)
GOOGLE_TOKEN_PATH = os.path.join(BACKEND_DIR, "token_calendar.json")
CALENDAR_SCOPES = ["https://www.googleapis.com/auth/calendar.events"]
_cal_state: dict = {}

import app.observability  # noqa: F401
from app.classify import classify_transcript
from app.extract import extract_thought
from app.llm import list_extractors
from app.schemas import Session, SuggestRequest, ExecuteRequest

app = FastAPI(title="Thought Galaxy")
app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],
    allow_methods=["*"],
    allow_headers=["*"],
)

SESSIONS: dict[str, Session] = {}
SESSIONS_EXTRACT: dict[str, dict] = {}


def _merge_session(existing, new):
    """Accumulate committed thoughts into one session blob so /suggest can
    find any bubble on the map, not just the latest thought's topics."""
    if not existing:
        return new
    merged = dict(existing)
    # topics: union by lowercased name; a newer thought about the same topic wins
    by_name = {(t.get("name") or "").lower(): t for t in merged.get("topics", [])}
    for t in new.get("topics", []):
        by_name[(t.get("name") or "").lower()] = t
    merged["topics"] = list(by_name.values())
    # concerns / actionItems / events: append (dedupe concerns by value)
    existing_concerns = set(merged.get("concerns", []))
    for c in new.get("concerns", []):
        if c not in existing_concerns:
            merged.setdefault("concerns", []).append(c)
            existing_concerns.add(c)
    merged["actionItems"] = merged.get("actionItems", []) + new.get("actionItems", [])
    merged["events"] = merged.get("events", []) + new.get("events", [])
    return merged


@app.get("/health")
def health():
    return {
        "ok": True,
        "extractors": list_extractors(),
        "claude_configured": bool(os.environ.get("ANTHROPIC_API_KEY", "").strip()),
    }


# ─────────────────────────── Milestone 1 ───────────────────────────
@app.websocket("/ws/transcribe")
async def ws_transcribe(ws: WebSocket):
    await ws.accept()
    from app.deepgram_stream import make_live_connection

    loop = asyncio.get_event_loop()
    accumulated: list[str] = []
    sid = str(uuid.uuid4())
    ws_graph: dict | None = None
    ws_request_actions = False
    ws_existing = []
    ws_subtopics: dict = {}

    def on_transcript(text: str, is_final: bool):
        if is_final:
            accumulated.append(text)
        live = " ".join(accumulated)
        if not is_final and text:
            live = (live + " " + text).strip()
        asyncio.run_coroutine_threadsafe(
            ws.send_json({"type": "partial", "text": live.strip()}), loop
        )

    def on_utterance_end():
        chunk = " ".join(accumulated).strip()
        if not chunk:
            return
        asyncio.run_coroutine_threadsafe(
            ws.send_json({"type": "partial_final", "text": chunk}), loop
        )

    dg = await make_live_connection(on_transcript, on_utterance_end)

    try:
        while True:
            msg = await ws.receive()
            if msg["type"] == "websocket.disconnect":
                break
            if msg.get("bytes") is not None:
                try:
                    dg.send(msg["bytes"])
                except Exception as e:
                    print(f"[ws] dg.send failed: {e}")
                    break
            elif msg.get("text") is not None:
                text = msg["text"]
                try:
                    ctrl = json.loads(text) or {}
                    done = ctrl.get("type") == "done"
                    if isinstance(ctrl.get("graph"), dict):
                        ws_graph = ctrl["graph"]
                    if "request_actions" in ctrl:
                        ws_request_actions = bool(ctrl["request_actions"])
                    if "existing_topics" in ctrl:
                        ws_existing = ctrl.get("existing_topics", [])
                    if "existing_subtopics" in ctrl:
                        ws_subtopics = ctrl.get("existing_subtopics", {})
                except Exception:
                    done = text == "done"
                if done:
                    break
    except WebSocketDisconnect:
        pass
    finally:
        try:
            await dg.finish()
        except Exception as e:
            print(f"[ws] dg.finish failed: {e}")
        full = " ".join(accumulated).strip()
        try:
            if full:
                result = extract_thought(full, ws_existing, graph=ws_graph, request_actions=ws_request_actions, existing_subtopics=ws_subtopics)
                await ws.send_json({"type": "extraction", "data": result})
            await ws.close()
        except Exception as e:
            print(f"[ws] finalize failed: {e}")


@app.post("/extract-thought")
def extract(payload: dict):
    text = (payload.get("text") or payload.get("transcript") or "").strip()
    if not text:
        return {"error": "text is required"}
    existing = payload.
[truncated — 9251 more characters]
```

### frontend/vite.config.js

```javascript
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
export default defineConfig({ plugins: [react()], server: { port: 5173 } });

```

### agents/test_calendar.py

```python
"""test_calendar.py — quick test for the calendar agent's direct path.

IMPORTANT: run this from the REPO ROOT (the folder that contains both
`backend/` and `agents/` as siblings), not from inside agents/ or backend/:

    python3 -m agents.test_calendar

First run will pop open a browser window asking you to log in and authorize —
say yes. After that, it caches a token in backend/token_calendar.json and
won't ask again.
"""
from dotenv import load_dotenv
load_dotenv("backend/.env")  # .env lives in backend/, explicit path needed

from agents.calendar_agent import handle_calendar_task

result = handle_calendar_task(
    text="Study for calc final",
    detail="Review chapters 6-9 before Tuesday",
)

print(result)
if result.get("ok"):
    print(f"\n✅ Event created! Check your Google Calendar, or open: {result['link']}")
else:
    print(f"\n❌ Failed: {result.get('error')}")
```

### agents/insight_agent.py

```python
"""Milestone 2 — Insight Agent (Fetch.ai uAgent).

This is your first real agent and the easiest to demo. It takes a bubble the
user tapped, reaches into past-session memory, and returns one grounded
suggestion. Registering it on Agentverse is what makes the Fetch.ai prize
real — the judges want to see an actual discoverable agent, not just an API
call dressed up as one.

Run:  python agents/insight_agent.py
It prints its address on startup — copy that into your .env as
INSIGHT_AGENT_ADDRESS so the backend can reach it.
"""
import os
from uagents import Agent, Context, Model
from dotenv import load_dotenv
load_dotenv()


# ─── message contract ───
class SuggestQuery(Model):
    node_text: str
    node_detail: str
    map_summary: str       # the rest of the current bubbles, as text
    past_context: str      # related past moments, pre-fetched by backend


class SuggestReply(Model):
    suggestion: str
    used_past: bool


insight_agent = Agent(
    name="thought_galaxy_insight",
    seed=os.environ.get("INSIGHT_AGENT_SEED", "insight-dev-seed"),
    port=8001,
    endpoint=["http://127.0.0.1:8001/submit"],
)


@insight_agent.on_event("startup")
async def announce(ctx: Context):
    ctx.logger.info(f"Insight Agent address: {insight_agent.address}")


@insight_agent.on_message(model=SuggestQuery, replies=SuggestReply)
async def on_query(ctx: Context, sender: str, msg: SuggestQuery):
    """Ask Claude for one grounded next step, given map + past context."""
    from anthropic import Anthropic

    client = Anthropic(api_key=os.environ["ANTHROPIC_API_KEY"])
    model = os.environ.get("CLAUDE_MODEL", "claude-sonnet-4-6")

    prompt = f"""The person tapped this bubble and wants one concrete next step.

TAPPED: {msg.node_text} — {msg.node_detail}

REST OF THEIR MAP:
{msg.map_summary}

RELATED PAST MOMENTS:
{msg.past_context}

Give ONE grounded next step in 2-4 warm sentences. Reference something real \
from the map or past. Not a therapist; offer a small doable action or gentle \
reframe. No lists."""

    resp = client.messages.create(
        model=model, max_tokens=400,
        messages=[{"role": "user", "content": prompt}],
    )
    await ctx.send(
        sender,
        SuggestReply(
            suggestion=resp.content[0].text.strip(),
            used_past=bool(msg.past_context.strip()),
        ),
    )


if __name__ == "__main__":
    insight_agent.run()

```

[17 more indexed source files omitted to keep this export small. The full file list is in the Codebase structure section above.]