# Project export: Busy Brain

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: The calendar that explains itself — and finds the time, the energy, and the place that actually work for you.
- Devpost: https://devpost.com/software/busy-brain
- GitHub: https://github.com/miaaoyama/BusyBrain.git
- Video: https://www.youtube.com/embed/p2DSnnO-nak?enablejsapi=1&hl=en_US&rel=0&start=&version=3&wmode=transparent
- Team: 1 GitHub contributor(s) — Marina Tribolet (7 commits)

## Devpost submission (written by the team)

### Overview

About BusyBrain

### Inspiration

I'm a Computer Engineering student who runs on a busy schedule, which means a calendar that just tells me when something is happening has never been the problem. The problem is everything invisible around the event: the travel time, the study blocks I never schedule, the social plans that fall apart because nobody accounts for how drained everyone already is by 6pm.

### What it does

BusyBrain is a multi-agent planning system that doesn't just place events on a calendar. It explains why it placed them there. Every study block, workout, and meal reminder comes with a real reason computed from my actual energy windows: "matches your high-energy window," or "your energy is only medium here, lower than ideal for this task." A dedicated "Today's Flow" view turns that same logic into a real energy chart for the day, with each task scored as a Peak Match, Energy Dip, or Steady fit against my actual schedule. It's not a decorative gauge, it's an honest computation. When I want to meet a friend, BusyBrain ranks shared time windows using real signals: calendar load, transition gaps, and how demanding the prior event was. It explains exactly why one slot beats another. It then searches the live web (via Browserbase) for a real nearby venue, pulling actual address, hours, and phone number from the venue's own site, with a layered fallback (fast fetch, then a full browser session, then an honest "nothing found") so it never fabricates details about a place that doesn't exist online the way you'd expect. How I built it The backbone is a multi-agent system on Fetch.ai's uAgents framework. An orchestrator fans a request out to three specialist agents (study, calendar, wellness) registered on the Almanac, each calling ASI:One's hosted LLM, with the orchestrator synthesizing their responses into one plan. On top of that sits a deterministic scheduling core: energy-window matching, focus-block splitting, drive-time buffers. That means the "why this time" explanations are literally the scoring logic that picked the slot, not an LLM guessing after the fact. The frontend started as a Claude Design mockup to nail the visual identity (a soft jellycat aesthetic) before any backend existed, then got rebuilt as plain HTML/JS talking to a real FastAPI layer. Same design, but every number on screen is now a real computation. For location intelligence, I integrated Browserbase across three of its primitives: search.web() to find candidate venues, fetch_api for fast page reads, and a Stagehand browser session as an automatic fallback when a site's bot protection blocks simple fetching. I also built real content-extraction logic that searches fetched pages for address, phone, and hours patterns and explicitly skips promotional banner text. That's the difference between a demo that looks impressive once and one that's actually reliable across a dozen different sites. Challenges I ran into A four-hour debugging chain that came down to one stray environment variable. A placeholder API key exported into a shell profile months earlier silently overrode the correct .env value, breaking ASI:One auth in a way that looked like a credentials problem but wasn't. Two of my four uAgents never registered an Agentverse mailbox, invisible until I checked each agent's dashboard individually. Real websites fight back. Early venue lookups returned bot checkpoint pages instead of content. I built a tiered fallback (cheap fetch, then full browser, then honest failure) and smarter extraction that scores sentences by whether they contain a real address, phone, or hours pattern, actively skipping promotional text that happened to load first. Pika MCP access was blocked for live video generation across every path I tried. The chat based MCP connection, the developer API signup, and the hackathon's dedicated Claude Code setup flow all hit account tier restrictions, with the final attempt blocked by an authentication issue before it could complete. I did successfully generate one video through Pika MCP using a licensed stock reference image, confirming the integration itself works end to end. The blocker was specifically live, in app generation access on my account tier, not the technical integration. Rather than fake a live connection that wasn't real, the app's invite flow honestly reflects this: it shows the real permission gating UI and a "sent to Pika" confirmation rather than claiming a video renders live in app. What I learned The redundancy critique was right, and fixing it taught the actual lesson: an agent earns its place by being accountable for its decisions, not just fast at making them. A static calendar app can place a block on a grid. Mine has to be able to say why, and mean it, even when that means admitting a feature didn't fully work.

## README (from the GitHub repository)

# BusyBrain Multi-Agent Planner

![tag:innovationlab](https://img.shields.io/badge/innovationlab-3D8BD3)
![tag:hackathon](https://img.shields.io/badge/hackathon-5F43F1)

BusyBrain turns an overwhelming week into a humane plan. Instead of producing a
generic to-do list, one public orchestrator coordinates three independently
registered Fetch.ai uAgents that reason about study workload, calendar logistics,
and sustainable pacing. The final answer includes the invisible work that ordinary
calendars miss: setup, travel, food, transitions, breaks, and recovery.

## Why agents?

The specialists have intentionally different goals. The Study Planner protects
learning progress, the Calendar Planner protects time constraints, and the Wellness
Planner protects the student's capacity. The orchestrator must reconcile their
recommendations into one usable plan. This makes agent coordination central to the
product rather than an eligibility wrapper around a chatbot.

## Agent network

| Agent | Responsibility | Port | Address |
|---|---|---:|---|
| [BusyBrain Orchestrator](https://agentverse.ai/agents/details/agent1q2qrnqd6v20qx0ltq2pnt2r85fadkekgs2js07w84h9jj87lhgm2crewv4j/profile) | ASI:One Chat Protocol entry point and final synthesis | 8001 | `agent1q2qrnqd6v20qx0ltq2pnt2r85fadkekgs2js07w84h9jj87lhgm2crewv4j` |
| [Study Planner](https://agentverse.ai/agents/details/agent1qdchqqjrvhyvx9cxxtmgwy498zwscn9ac9yqja43cqv90w3q3asq70rjrww/profile) | Focus rhythms, workload, deadlines, and daily study limits | 8002 | `agent1qdchqqjrvhyvx9cxxtmgwy498zwscn9ac9yqja43cqv90w3q3asq70rjrww` |
| [Calendar Planner](https://agentverse.ai/agents/details/agent1qvv9585yydz2r9p3n245t9y6l46kqpsa0e299n93nl5y60f6lgfwx64wygz/profile) | Fixed events, conflicts, travel, preparation, and transitions | 8003 | `agent1qvv9585yydz2r9p3n245t9y6l46kqpsa0e299n93nl5y60f6lgfwx64wygz` |
| [Workload & Social Context](https://agentverse.ai/agents/details/agent1q0pkvdgsxv4w8r5lqh2chmlhantxtlk28e5yh9g06ah5xwnuvqznqzenkwe/profile) | Calendar-pressure signals, fair meetup timing, recovery, and rationale | 8004 | `agent1q0pkvdgsxv4w8r5lqh2chmlhantxtlk28e5yh9g06ah5xwnuvqznqzenkwe` |

## End-to-end flow

1. A user sends a natural-language request from ASI:One.
2. BusyBrain acknowledges the Chat Protocol message and creates a request ID.
3. It dispatches a typed `PlanningRequest` to the Study, Calendar, and Workload & Social specialists.
4. Each specialist returns a signed `SpecialistResponse`.
5. BusyBrain verifies each sender, waits for all three, resolves conflicts, and
   returns ranked options through the Chat Protocol.
6. The user replies `choose option 1`, `choose option 2`, or `choose option 3`.
7. BusyBrain persists a confirmed-event action receipt and returns a populated
   Google Calendar action link—all inside the ASI:One conversation.

If a specialist is unavailable, the orchestrator finalizes after 45 seconds with
the verified partial results and names the missing specialist instead of hanging.

## Local setup

Python 3.12 or 3.13 is recommended.

```bash
python3.13 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
cp .env.example .env
```

Add an ASI:One key and four unique private seeds to `.env`. Never commit `.env`;
changing a seed after registration changes that agent's address.

Run each agent in its own terminal:

```bash
make orchestrator
make study
make calendar
make workload
```

Open each Inspector URL, choose **Connect → Mailbox**, and keep all four processes
running for the live demo. Each agent needs its own mailbox.

Before testing in ASI:One, validate the exact same Chat Protocol route locally in a
fifth terminal:

```bash
make smoke-test
```

The client prints `TEST PASSED` after the orchestrator has collected all three
specialist responses and returned the final plan. Press Ctrl+C after the result.

## ASI:One demo prompt

> Find me 45 minutes with Maya in the next seven days, preferably in the afternoon
> and not right after class. Compare both calendars, give me the three best options,
> and explain why the top time is fair to both of us.

Then reply:

> choose option 2

The terminal logs show the orchestrator dispatching the request and receiving three
specialist responses before returning the synthesized plan. The Workload & Social
agent calls the deterministic ranking engine against seeded calendars, so the times
and fit scores are evidence rather than invented availability. The second turn
produces a durable confirmation receipt and a Google Calendar action.

## Pika invitation handoff

After the user approves a meetup time, the Streamlit experience prepares a
consent-gated Pika invitation using a saved cartoon avatar rather than a real
person's face. The creative concept turns her wrist calendar into a glowing
jellyfish portal whose bubbles assemble into the confirmed date, time, and place.

The Pika MCP workflow is deliberately multi-step: generate a short spoken invitation,
animate the approved avatar to that audio, and render the confirmed event details as
stable mobile-safe text. The output is an 8-second vertical clip displayed beside the
calendar confirmation. Scheduling remains independent, so a media-provider delay can
never lose or invalidate the confirmed event. This combines an original visual story,
privacy-aware generation, and a shareable action outcome instead of treating video as
decoration.

## Existing product prototype

The repository also contains a Streamlit interface and deterministic scheduling
engine:

```bash
streamlit run app.py
python main.py
```

These modules demonstrate the broader product direction; the Agentverse workflow is
implemented by `agent.py`, `study_agent.py`, `calendar_agent.py`, and
`wellness_agent.py`.

For the complete browser demo (calendar UI + interactive Pika message), run:

```bash
.venv/bin/uvicorn api:app --reload --port 8000
python3 -m http.server 8080
```

Then open `http://localhost:8080/`. Keep both processes running during the demo.

To start the website, API, and all four Fetch.ai agents from one terminal instead:

```bash
make app
```

Press `Ctrl+C` once to stop every Busy Brain process.

## Safety and scope

BusyBrain provides planning support, not medical diagnosis, therapy, or professional
academic advice. Specialist responses are treated as recommendations, and missing
schedule information is surfaced as an assumption rather than silently invented.

## Hackathon submission checklist

- [ ] Public GitHub repository
- [x] Four Agentverse profile links documented
- [ ] Shared ASI:One chat session
- [ ] Three-to-five-minute demo video
- [ ] Devpost explanation and architecture

## Tests

```bash
python -m unittest discover -s tests -v
```

The suite covers natural-language constraints, collision prevention, recovery-gap
ranking, option selection, durable confirmation, and Google Calendar handoff URLs.


## Detected evidence (automated analysis)

Indexed codebase: 73 recognized source files, 1075 KB.
- FastAPI (technology) — detected in the code
- HTML (language) — detected in the code
- JavaScript (language) — detected in the code
- OpenAI (technology) — detected in the code
- Python (language) — detected in the code
- Streamlit (technology) — detected in the code
- Anthropic (technology) — claimed on Devpost, not found in the code
- CSS (language) — claimed on Devpost, not found in the code

## Codebase structure (from repository index)

### Files (78 of 78)

```
.agents/skills/app-sizzle/references/liquid-glass.md
.agents/skills/app-sizzle/references/template-a-cinematic.md
.agents/skills/app-sizzle/SKILL.md
.agents/skills/app-store-screens/references/default-layout.md
.agents/skills/app-store-screens/references/layout-archetypes.md
.agents/skills/app-store-screens/references/render-pipeline.md
.agents/skills/app-store-screens/SKILL.md
.agents/skills/baseball-trend/SKILL.md
.agents/skills/build-a-brand/references/brand-directions.md
.agents/skills/build-a-brand/references/brand-guidelines.md
.agents/skills/build-a-brand/references/brand-identity.md
.agents/skills/build-a-brand/references/brand-md-template.md
.agents/skills/build-a-brand/SKILL.md
.agents/skills/content-director/formats/dance.md
.agents/skills/content-director/formats/duet.md
.agents/skills/content-director/formats/pov.md
.agents/skills/content-director/formats/talking.md
.agents/skills/content-director/formats/teleprompter.html
.agents/skills/content-director/formats/teleprompter.md
.agents/skills/content-director/README.md
.agents/skills/content-director/SKILL.md
.agents/skills/explainer/SKILL.md
.agents/skills/fix-my-look/SKILL.md
.agents/skills/founder-product-video/references/ops-notes.md
.agents/skills/founder-product-video/SKILL.md
.agents/skills/kiss-cam/SKILL.md
.agents/skills/language-swap/references/language-coverage.md
.agents/skills/language-swap/SKILL.md
.agents/skills/persona-builder/references/aesthetic-prompts.md
.agents/skills/persona-builder/references/persona-md-template.md
.agents/skills/persona-builder/references/templates/mood-board-no-header.template.html
.agents/skills/persona-builder/references/templates/mood-board.template.html
.agents/skills/persona-builder/references/templates/pdf-about.template.html
.agents/skills/persona-builder/references/templates/pdf-content-categories.template.html
.agents/skills/persona-builder/references/templates/pdf-cover.template.html
.agents/skills/persona-builder/references/templates/pdf-do-dont.template.html
.agents/skills/persona-builder/references/templates/pdf-hooks-dm.template.html
.agents/skills/persona-builder/references/templates/pdf-moodboard.template.html
.agents/skills/persona-builder/references/templates/pdf-next-steps.template.html
.agents/skills/persona-builder/references/templates/pdf-shared-head.template.html
.agents/skills/persona-builder/references/templates/pdf-voice-mode.template.html
.agents/skills/persona-builder/SKILL.md
.agents/skills/podcast/SKILL.md
.agents/skills/ugc-ads/SKILL.md
.agents/skills/viral-hook/SKILL.md
.env.example
.gitignore
agent_config.py
agent_messages.py
agent_utils.py
agent.py
agents.py
api.py
app.py
cafe_finder.js
calendar_actions.py
calendar_agent.py
calm-portal/index.html
coordination.py
index.html
main.py
Makefile
models.py
README.md
requirements.txt
sample_data.py
scheduler.py
skills-lock.json
smoke_test_agent.py
start-busy-brain.sh
study_agent.py
test_fetch_sites.py
test_venue_queries.py
tests/test_api_meetings.py
tests/test_calendar_actions.py
tests/test_coordination.py
venue_lookup.py
wellness_agent.py
```

### Dependencies

- requirements.txt: browserbase@>=1.13, fastapi@>=0.115, openai@>=1.0, python-dotenv@>=1.0, stagehand@>=3.21, streamlit@>=1.31, uagents@>=0.22, uvicorn@>=0.30

### Recent commits (newest first)

- Add one-command Busy Brain launcher
- Add interactive Pika invite and persistent calendar flow
- Add Today's Flow view, Browserbase venue lookup improvements, rename images to assets
- Update Busy Brain UI and scheduling experience
- Redesign BusyBrain interface
- Complete Fetch AI multi-agent scheduling workflow
- surface scheduling reasoning in notes
- initial commit
- initial commit

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

### .agents/skills/viral-hook/SKILL.md

```markdown
---
name: viral-hook
description: >-
  Prepend a 4s viral hook + optional designed title to a user's video, then hard-cut into
  the real clip. An attention-grabbing event erupts into the user's OWN scene; the title is
  rendered in-scene by Seedance. One MCP call does the whole render. Requires Pika MCP.
  Triggers: "add a viral hook", "disruption hook on my video", "hook + title on my clip",
  "viral hook with typography", "scroll-stopper intro".
argument-hint: "[video path or URL] [optional title line]"
required-capabilities:
  - mcp__plugin_pika_pika__upload_asset
  - mcp__plugin_pika_pika__analyze_media
  - mcp__plugin_pika_pika__generate_viral_hook
  - mcp__plugin_pika_pika__task_status
---

# Viral Hook

Prepends an extreme, no-dialogue ~4s hook to a user's video — an attention-grabbing event
that erupts into the user's own scene, with an optional designed title burned into the
lower third — then hard-cuts into the untouched clip.

`mcp__plugin_pika_pika__generate_viral_hook` does the whole render in one call and returns BOTH
the hook and the stitched final. This skill's job is the creative judgment the tool can't
make: read the scene, pick a hook action from the menu, and write a title.

## Hook rules (govern what you WRITE as `hook_action`)

1. **Certain to grab attention, fast.** Extreme and unmissable inside ~2s — over plausibility
   or relevance to the video's topic.
2. **Erupts into the user's OWN scene.** Same person, location, lighting — the tool anchors
   on the best visual anchor frame: 0s when usable, otherwise the first detectable face frame.
   Describe the event entering THAT space, never a different setting.
3. **Use the scene's real geometry.** Enter through a doorway with depth, a wall behind the
   subject, headroom above.
4. **No dialogue.** Voice-free (only ambient / impact SFX) — don't write spoken lines into
   `hook_action`.

## Hook super-category menu

Four super-categories. **Rotate** super-category per run on the same clip/session — without
rotation, regenerates collapse to vehicle / explosion / creature. Archetypes are starting
points, not a closed list; invent freely within the category.

### A. Destructive intrusion — external force violently enters and damages the scene

| Archetype | What happens | Scene affordance |
|---|---|---|
| Incoming vehicle | car / truck / bus smashes through a wall or doorway and barrels at camera | background depth or a back wall |
| Flash flood / wave | a wall bursts and water surges down toward camera | corridor / back wall |
| Structural collapse | ceiling / wall / shelf caves in, debris rains down | headroom / walls |
| Explosion / blast | fireball erupts behind them, shockwave + debris rush forward | a background surface |
| Creature attack | animal / monster lunges from off-frame or bursts in | open off-frame edge |
| Absurd intruder | dinosaur, elephant, UFO, giant hand enters behind them | background space |
| Natural disaster | tornado / meteor / lightning / sinkhole | 
[truncated — 6089 more characters]
```

### .agents/skills/podcast/SKILL.md

```markdown
---
name: podcast
description: >-
  Two-host podcast video for any URL or free-form topic — 1 minute, 4 acts × ~15s,
  native multi-shot dialogue, optional voice cloning for Host A. Use when the user
  asks to "make a podcast", "podcast about [thing]", "podcast review of [url]",
  "two-host explainer", "interview-style clip", "two people talking on camera",
  "I/me and X talk about Y", or "interview with [persona] about [topic]". Native
  audio is the deliverable; captions are skipped by default because podcast dialogue
  mistranscribes domain terms.
argument-hint: <url-or-topic> [bg_img=] [host_a_img=] [host_b_img=] [voice_a=] [voice_b=] [use_avatar] [aspect_ratio=16:9]
---

# /pika:podcast

4 acts × 15s each = 60s. Host A always LEFT, Host B always RIGHT. Accepts a URL **or** a free-form topic / brief.

## Parameters

| Param | Default | Notes |
|---|---|---|
| `input` | required | URL to review **or** free-form topic / brief (e.g. "I and Elon Musk talk about Mars") |
| `bg_img` | auto-generated | Podcast studio background |
| `host_a_img` | auto-generated | Host A portrait — see Real-person handling below |
| `host_b_img` | auto-generated | Host B portrait — see Real-person handling below |
| `voice_a` | `876341503281471517` | Kling preset or cloned voice ID for Host A |
| `voice_b` | `829837252279803904` | Kling preset or cloned voice ID for Host B |
| `use_avatar` | off | Clone user's identity voice as Host A via `clone_voice` |
| `aspect_ratio` | `16:9` | Output aspect ratio |

## Defaults — fire fast, no mid-flow confirmation

- **Use the param-table defaults silently for voices.** `voice_a` defaults to the Kling preset `876341503281471517` and `voice_b` to `829837252279803904`. Do **not** ask "which voice?" or "should I clone yours?" before firing — only honor explicit overrides (`voice_a=`, `voice_b=`, `use_avatar`).
- **Auto-generate any missing host portraits silently** (Step 1's archetype prompts). Do **not** ask "should I generate a host image?" — just generate.
- **No "type yes to proceed" gates.** Submit → render the 4 acts → return URL. Account credit balance + provider failover are the canonical guardrails. The `--yes` flag is accepted as a no-op for backward compatibility.
- **Topic-mode personas (Step 3)** — when the user names a real public figure, follow Step 4 (Real-person handling) silently: archetype portrait by default, no auto-generated photographic likeness, no question to the user about likeness rights.

## Local images on Claude Desktop

Claude Desktop can't pass inline-pasted images to MCP tools yet (Anthropic-side limitation). If the user pastes a photo inline, or mentions a local file they want as `host_a_img` / `host_b_img`, pause Step 1 and kindly send them this — something like:

> Heads up — pasted images don't reach MCP tools on Claude Desktop yet (Anthropic limitation). Two easy options for your photo:
>
> - **Paste a URL** if it's already hosted (Imgur, S3, your site) — fastest
> - **Attach the image file** 
[truncated — 9613 more characters]
```

### requirements.txt

```
# Web interface
streamlit>=1.31
fastapi>=0.115
uvicorn>=0.30

# Fetch.ai multi-agent network
uagents>=0.22
openai>=1.0
python-dotenv>=1.0

# Browserbase venue search, fetch, and Stagehand fallback
browserbase>=1.13
stagehand>=3.21

```

### main.py

```python
from sample_data import build_sample_inputs
from agents import CalendarOrchestrator


def print_plan(result):
    current_day = None
    for event in result.events:
        if event.start.date() != current_day:
            current_day = event.start.date()
            print(f"\n=== {current_day.strftime('%A, %b %d')} ===")
        print(
            f"{event.start.strftime('%I:%M %p')} - {event.end.strftime('%I:%M %p')} | "
            f"{event.title} [{event.task_type.value}]"
        )
        if event.notes:
            print(f"   → {event.notes}")

    if result.messages:
        print("\nMessages:")
        for msg in result.messages:
            print(f"- {msg}")

    if result.unscheduled:
        print("\nNeeds manual review:")
        for item in result.unscheduled:
            print(f"- {item}")


def main():
    print("Busy Brain")
    profile, classes, fixed_events, tasks, week_start = build_sample_inputs()
    orchestrator = CalendarOrchestrator()
    result = orchestrator.build_week_plan(profile, classes, fixed_events, tasks, week_start)
    print_plan(result)

    friend_result = orchestrator.social_agent.find_friend_meeting(
        friend_a_events=result.events,
        friend_b_events=[],
        profile=profile,
        start_day=week_start,
        end_day=week_start.replace(day=week_start.day) if False else week_start,
        duration_minutes=90,
    )
    if friend_result.events:
        print("\nSuggested friend meeting:")
        print_plan(friend_result)


if __name__ == "__main__":
    main()

```

### app.py

```python
import base64
import html
import importlib
import math
from datetime import date, datetime, timedelta

import streamlit as st

import sample_data
from agents import CalendarOrchestrator
from models import CalendarEvent, EnergyLevel, EnergyWindow, TaskType
from scheduler import find_best_slot


st.set_page_config(page_title="Busy Brain", page_icon="🧠", layout="wide")

# Streamlit may retain an older module during development hot reloads.
sample_data = importlib.reload(sample_data)
build_sample_inputs = sample_data.build_sample_inputs
build_friend_schedules = sample_data.build_friend_schedules


def data_uri(path: str, mime: str) -> str:
    with open(path, "rb") as file:
        return f"data:{mime};base64,{base64.b64encode(file.read()).decode()}"


CAT_BG = data_uri("images/cat-bg.jpg", "image/jpeg")
CAT_CIRCLE = data_uri("images/cat-circle.png", "image/png")
CAT_PLUS = data_uri("images/cat-plus-launcher-reference.png", "image/png")

COLORS = {
    "class": "#a78bfa",
    "study": "#60a5fa",
    "appointment": "#fb7185",
    "workout": "#34d399",
    "meal": "#fbbf24",
    "social": "#818cf8",
    "personal": "#818cf8",
    "travel": "#94a3b8",
}
TYPE_LABELS = {
    "class": "Class",
    "study": "Study",
    "appointment": "Appointment",
    "workout": "Workout",
    "meal": "Meal",
    "social": "Social",
    "personal": "Personal",
    "travel": "Travel",
}
LOADS = {
    "class": (2, "Medium focus"),
    "study": (3, "Heavy focus"),
    "appointment": (2, "Medium focus"),
    "workout": (2, "Medium focus"),
    "meal": (1, "Light focus"),
    "social": (1, "Light focus"),
    "personal": (1, "Light focus"),
    "travel": (1, "Light focus"),
}


def init_state() -> None:
    defaults = {
        "view": "Today's flow",
        "selected_day": date.today(),
        "energy_peak": "Morning",
        "class_name": "Computer Science",
        "weekly_study_hours": 10,
        "saved_meetings": [],
        "saved_tasks": [],
        "added_classes": [],
        "friend_results": [],
        "chosen_meeting": None,
        "invite_ready": False,
    }
    for key, value in defaults.items():
        if key not in st.session_state:
            st.session_state[key] = value


def energy_at(minute: float, peak: str | None = None) -> float:
    peak = peak or st.session_state.energy_peak

    def gaussian(center: float, width: float) -> float:
        return math.exp(-((minute - center) ** 2) / (2 * width**2))

    value = 0.2
    if peak == "Morning":
        value += 0.62 * gaussian(600, 115) + 0.30 * gaussian(980, 135)
    elif peak == "Afternoon":
        value += 0.64 * gaussian(860, 150) + 0.22 * gaussian(600, 100)
    else:
        value += 0.58 * gaussian(1160, 140) + 0.30 * gaussian(620, 120)
    value -= 0.20 * gaussian(790, 55)
    return max(0.08, min(1.0, value))


def curve_paths(width: int, top: int, baseline: int, peak: str | None = None):
    points = []
    for minute in range(420, 1261, 12):
        x = ((minute - 420) / 840) * width
        y = baseline - energy_at(minute, peak) * (baseline - top)
        points.append((x, y))
    line = " ".join(
        f"{'M' if index == 0 else 'L'}{x:.1f} {y:.1f}"
        for index, (x, y) in enumerate(points)
    )
    return line, f"{line} L{width} {baseline} L0 {baseline} Z"


def task_kind(event: CalendarEvent) -> str:
    if event.title.startswith("Drive"):
        return "travel"
    return event.task_type.value if hasattr(event.task_type, "value") else str(event.task_type)


def match_for(event: CalendarEvent):
    minute = (event.start.hour * 60 + event.start.minute + event.end.hour * 60 + event.end.minute) / 2
    level = energy_at(minute)
    load = LOADS.get(task_kind(event), (1, "Light focus"))[0]
    if load >= 3 and level < 0.5:
        return "⚠", "Energy dip", "#b4791a", "#fff3dc", "low"
    if load >= 2 and level < 0.38:
        return "⚠", "Low energy", "#b4791a", "#fff3dc", "low"
    if level >= 0.6:
        return "✓", "Peak match", "#1f9d6b", "#e6f7ef", "good"
    return "~", "Steady", "#6d56e8", "#efecfb", "ok"


def build_plan():
    profile, classes, fixed_events, tasks, week_start = build_sample_inputs()
    classes[0].class_name = st.session_state.class_name
    classes[0].weekly_study_hours = st.session_state.weekly_study_hours
    for meeting in classes[0].meetings:
        meeting.title = f"{st.session_state.class_name} Class"

    if st.session_state.energy_peak == "Afternoon":
        profile.energy_windows = [
            EnergyWindow(datetime.min.replace(hour=9).time(), datetime.min.replace(hour=11).time(), EnergyLevel.MEDIUM),
            EnergyWindow(datetime.min.replace(hour=13).time(), datetime.min.replace(hour=17).time(), EnergyLevel.HIGH),
        ]
    elif st.session_state.energy_peak == "Evening":
        profile.energy_windows = [
            EnergyWindow(datetime.min.replace(hour=10).time(), datetime.min.replace(hour=13).time(), EnergyLevel.MEDIUM),
            EnergyWindow(datetime.min.replace(hour=17).time(), datetime.min.replace(hour=21).time(), EnergyLevel.HIGH),
        ]

    result = CalendarOrchestrator().build_week_plan(profile, classes, fixed_events, tasks, week_start)
    result.events.extend(
        meeting for meeting in st.session_state.saved_meetings if hasattr(meeting, "start")
    )
    result.events.sort(key=lambda event: event.start)
    return profile, result, week_start


init_state()

st.markdown(
    f"""
<style>
@import url('https://fonts.googleapis.com/css2?family=Nunito:wght@400;500;600;700;800;900&display=swap');
* {{ box-sizing:border-box; }}
html, body, [class*="css"] {{ font-family:'Nunito',system-ui,sans-serif; }}
.stApp {{ background:#f4f2fb; color:#2d2b52; }}
[data-testid="stHeader"], [data-testid="stToolbar"] {{ background:transparent; }}
[data-testid="stSidebar"] {{ width:264px !important; background:#fcfbff; border-right:1px solid #ece7f5; }}
[data-testid="stSidebar"] > div:first-child {{ width:264px !important; padding:16px 16px 20px; }}
[data-testid="stSide
[truncated — 20032 more characters]
```

### agent_messages.py

```python
"""Typed messages exchanged by the BusyBrain uAgents."""

from uagents import Model
from pydantic.v1 import Field


class PlanningRequest(Model):
    request_id: str
    user_text: str


class MeetingOption(Model):
    option_id: int
    title: str
    start_iso: str
    end_iso: str
    score: int
    reasons: list[str] = Field(default_factory=list)
    tradeoffs: list[str] = Field(default_factory=list)


class SpecialistResponse(Model):
    request_id: str
    specialist: str
    result: str
    meeting_options: list[MeetingOption] = Field(default_factory=list)

```

### test_venue_queries.py

```python
"""Run several different venue queries through find_venue_for_meetup to see
overall hit rate across the fetch -> stagehand -> search-only fallback chain.
    python3 test_venue_queries.py
"""

from venue_lookup import find_venue_for_meetup

queries = [
    ("quiet coffee shop for studying", "UCI Irvine"),
    ("library study room", "Irvine CA"),
    ("casual lunch spot", "Saddleback College Mission Viejo"),
    ("walk and smoothie place", "Irvine CA"),
    ("museum or bookstore", "Irvine CA"),
]

for activity, location in queries:
    print(f"\n=== {activity} near {location} ===")
    try:
        result = find_venue_for_meetup(activity, location)
        if result:
            print(f"[{result.source}] {result.name}")
            print(f"  {result.url}")
            print(f"  {result.snippet[:150]}")
        else:
            print("  No result.")
    except Exception as e:
        print(f"  ERROR: {e}")
```

### agent_utils.py

```python
"""ASI:One helpers shared by the orchestrator and specialists."""

from __future__ import annotations

import asyncio

from openai import OpenAI

from agent_config import ASI1_API_KEY


def make_client() -> OpenAI:
    if not ASI1_API_KEY:
        raise RuntimeError("ASI1_API_KEY is missing. Add it to .env or export it.")
    return OpenAI(base_url="https://api.asi1.ai/v1", api_key=ASI1_API_KEY)


async def ask_asi(system_prompt: str, user_prompt: str, max_tokens: int = 900) -> str:
    """Call the synchronous OpenAI-compatible SDK without blocking the agent loop."""
    client = make_client()

    def complete():
        return client.chat.completions.create(
            model="asi1",
            messages=[
                {"role": "system", "content": system_prompt},
                {"role": "user", "content": user_prompt},
            ],
            max_tokens=max_tokens,
        )

    response = await asyncio.to_thread(complete)
    return str(response.choices[0].message.content)


```

### test_fetch_sites.py

```python
"""Quick test: does Browserbase's fetch_api get blocked on multiple sites,
or was the earlier result (corner.inc) a one-off? Run this directly:
    python3 test_fetch_sites.py
"""

import os
import re
from dotenv import load_dotenv
load_dotenv()

from browserbase import Browserbase

bb = Browserbase(api_key=os.getenv("BROWSERBASE_API_KEY"))

test_urls = [
    "https://www.starbucks.com/store-locator/store/1234",  # may 404, that's fine — testing block behavior
    "https://en.wikipedia.org/wiki/Coffeehouse",
    "https://www.yelp.com/biz/moongoat-coffee-irvine",
    "https://www.google.com/maps/search/coffee+shop+irvine",
    "https://www.peets.com/pages/store/u-c-irvine",
]

for url in test_urls:
    print(f"\n=== {url} ===")
    try:
        result = bb.fetch_api.create(url=url, format="markdown")
        content = getattr(result, "content", None) or getattr(result, "text", "") or str(result)
        content = re.sub(r"\s+", " ", content).strip()
        print(content[:300])
    except Exception as e:
        print(f"ERROR: {e}")

```

### agent_config.py

```python
"""Shared configuration for the BusyBrain uAgent network."""

from __future__ import annotations

import os

from uagents.crypto import Identity

try:
    from dotenv import load_dotenv

    load_dotenv()
except ImportError:
    # Environment exports still work when python-dotenv is not installed yet.
    pass

ASI1_API_KEY = os.getenv("ASI1_API_KEY", "")

def required_seed(name: str) -> str:
    value = os.getenv(name)
    if not value:
        raise RuntimeError(f"{name} is missing. Copy .env.example to .env and set it.")
    return value


ORCHESTRATOR_SEED = required_seed("BUSY_BRAIN_ORCHESTRATOR_SEED")
STUDY_SEED = required_seed("BUSY_BRAIN_STUDY_SEED")
CALENDAR_SEED = required_seed("BUSY_BRAIN_CALENDAR_SEED")
WELLNESS_SEED = required_seed("BUSY_BRAIN_WELLNESS_SEED")


def address_for(seed: str) -> str:
    return Identity.from_seed(seed, 0).address


ORCHESTRATOR_ADDRESS = address_for(ORCHESTRATOR_SEED)
STUDY_ADDRESS = address_for(STUDY_SEED)
CALENDAR_ADDRESS = address_for(CALENDAR_SEED)
WELLNESS_ADDRESS = address_for(WELLNESS_SEED)

SPECIALIST_ADDRESSES = {
    "study": STUDY_ADDRESS,
    "calendar": CALENDAR_ADDRESS,
    "wellness": WELLNESS_ADDRESS,
}

```

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