Spaces:
Running
Running
Commit ·
ea1db8d
0
Parent(s):
base
Browse files- .gitignore +156 -0
- README.md +228 -0
- __init__.py +16 -0
- client.py +31 -0
- models.py +144 -0
- openenv.yaml +99 -0
- pyproject.toml +45 -0
- server/Dockerfile +80 -0
- server/__init__.py +11 -0
- server/app.py +84 -0
- server/data.py +209 -0
- server/requirements.txt +8 -0
- server/second_brain_env_environment.py +104 -0
.gitignore
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Byte-compiled / optimized / DLL files
|
| 2 |
+
__pycache__/
|
| 3 |
+
*.py[cod]
|
| 4 |
+
*$py.class
|
| 5 |
+
|
| 6 |
+
# C extensions
|
| 7 |
+
*.so
|
| 8 |
+
|
| 9 |
+
# Distribution / packaging
|
| 10 |
+
.Python
|
| 11 |
+
build/
|
| 12 |
+
develop-eggs/
|
| 13 |
+
dist/
|
| 14 |
+
downloads/
|
| 15 |
+
eggs/
|
| 16 |
+
.eggs/
|
| 17 |
+
lib/
|
| 18 |
+
lib64/
|
| 19 |
+
parts/
|
| 20 |
+
sdist/
|
| 21 |
+
var/
|
| 22 |
+
wheels/
|
| 23 |
+
pip-wheel-metadata/
|
| 24 |
+
share/python-wheels/
|
| 25 |
+
*.egg-info/
|
| 26 |
+
.installed.cfg
|
| 27 |
+
*.egg
|
| 28 |
+
MANIFEST
|
| 29 |
+
|
| 30 |
+
# PyInstaller
|
| 31 |
+
# Usually these files are written by a python script from a template
|
| 32 |
+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
| 33 |
+
*.manifest
|
| 34 |
+
*.spec
|
| 35 |
+
|
| 36 |
+
# Installer logs
|
| 37 |
+
pip-log.txt
|
| 38 |
+
pip-delete-this-directory.txt
|
| 39 |
+
|
| 40 |
+
# Unit test / coverage reports
|
| 41 |
+
htmlcov/
|
| 42 |
+
.tox/
|
| 43 |
+
.nox/
|
| 44 |
+
.coverage
|
| 45 |
+
.coverage.*
|
| 46 |
+
.cache
|
| 47 |
+
nosetests.xml
|
| 48 |
+
coverage.xml
|
| 49 |
+
*.cover
|
| 50 |
+
*.py,cover
|
| 51 |
+
.hypothesis/
|
| 52 |
+
.pytest_cache/
|
| 53 |
+
|
| 54 |
+
# Translations
|
| 55 |
+
*.mo
|
| 56 |
+
*.pot
|
| 57 |
+
|
| 58 |
+
# Django stuff:
|
| 59 |
+
*.log
|
| 60 |
+
local_settings.py
|
| 61 |
+
db.sqlite3
|
| 62 |
+
db.sqlite3-journal
|
| 63 |
+
|
| 64 |
+
# Flask stuff:
|
| 65 |
+
instance/
|
| 66 |
+
.webassets-cache
|
| 67 |
+
|
| 68 |
+
# Scrapy stuff:
|
| 69 |
+
.scrapy
|
| 70 |
+
|
| 71 |
+
# Sphinx documentation
|
| 72 |
+
docs/_build/
|
| 73 |
+
|
| 74 |
+
# PyBuilder
|
| 75 |
+
target/
|
| 76 |
+
|
| 77 |
+
# Jupyter Notebook
|
| 78 |
+
.ipynb_checkpoints
|
| 79 |
+
|
| 80 |
+
# IPython
|
| 81 |
+
profile_default/
|
| 82 |
+
ipython_config.py
|
| 83 |
+
|
| 84 |
+
# pyenv
|
| 85 |
+
.python-version
|
| 86 |
+
|
| 87 |
+
# pipenv
|
| 88 |
+
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
| 89 |
+
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
| 90 |
+
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
| 91 |
+
# install all needed dependencies.
|
| 92 |
+
#Pipfile.lock
|
| 93 |
+
|
| 94 |
+
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
|
| 95 |
+
__pypackages__/
|
| 96 |
+
|
| 97 |
+
# Celery stuff
|
| 98 |
+
celerybeat-schedule
|
| 99 |
+
celerybeat.pid
|
| 100 |
+
|
| 101 |
+
# SageMath parsed files
|
| 102 |
+
*.sage.py
|
| 103 |
+
|
| 104 |
+
# Environments
|
| 105 |
+
.env
|
| 106 |
+
.venv
|
| 107 |
+
env/
|
| 108 |
+
venv/
|
| 109 |
+
ENV/
|
| 110 |
+
env.bak/
|
| 111 |
+
venv.bak/
|
| 112 |
+
|
| 113 |
+
# Spyder project settings
|
| 114 |
+
.spyderproject
|
| 115 |
+
.spyproject
|
| 116 |
+
|
| 117 |
+
# Rope project settings
|
| 118 |
+
.ropeproject
|
| 119 |
+
|
| 120 |
+
# mkdocs documentation
|
| 121 |
+
/site
|
| 122 |
+
|
| 123 |
+
# mypy
|
| 124 |
+
.mypy_cache/
|
| 125 |
+
.dmypy.json
|
| 126 |
+
dmypy.json
|
| 127 |
+
|
| 128 |
+
# Pyre type checker
|
| 129 |
+
.pyre/
|
| 130 |
+
|
| 131 |
+
# IDE
|
| 132 |
+
.vscode/
|
| 133 |
+
.idea/
|
| 134 |
+
*.swp
|
| 135 |
+
*.swo
|
| 136 |
+
*~
|
| 137 |
+
|
| 138 |
+
# OS
|
| 139 |
+
.DS_Store
|
| 140 |
+
.DS_Store?
|
| 141 |
+
._*
|
| 142 |
+
.Spotlight-V100
|
| 143 |
+
.Trashes
|
| 144 |
+
ehthumbs.db
|
| 145 |
+
Thumbs.db
|
| 146 |
+
|
| 147 |
+
# Logs
|
| 148 |
+
*.log
|
| 149 |
+
logs/
|
| 150 |
+
|
| 151 |
+
# Temporary files
|
| 152 |
+
*.tmp
|
| 153 |
+
*.temp
|
| 154 |
+
|
| 155 |
+
# Docker (if building locally)
|
| 156 |
+
.dockerignore
|
README.md
ADDED
|
@@ -0,0 +1,228 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# 🧠 Second Brain — OpenEnv Environment
|
| 2 |
+
|
| 3 |
+
> A Personal Knowledge Management environment where AI agents learn to capture, organize, and retrieve information — simulating the real-world challenge of managing a second brain.
|
| 4 |
+
|
| 5 |
+
[](https://github.com/meta-pytorch/OpenEnv)
|
| 6 |
+
[](https://huggingface.co/spaces/RAc1928/second-brain-env)
|
| 7 |
+
[](https://python.org)
|
| 8 |
+
|
| 9 |
+
---
|
| 10 |
+
|
| 11 |
+
## 🌍 Motivation
|
| 12 |
+
|
| 13 |
+
Millions of people struggle with **information overload** daily. We save articles, capture meeting notes, jot down ideas — but rarely retrieve them when we actually need them.
|
| 14 |
+
|
| 15 |
+
This environment trains AI agents to act as a **Personal Knowledge Manager**: capturing raw notes, organizing them correctly, and retrieving the right information at the right time. This is a genuine real-world skill that benefits students, professionals, researchers, and anyone who manages information.
|
| 16 |
+
|
| 17 |
+
---
|
| 18 |
+
|
| 19 |
+
## 🎯 Tasks
|
| 20 |
+
|
| 21 |
+
### Task 1 — `note_categorization` 🟢 Easy
|
| 22 |
+
| Property | Value |
|
| 23 |
+
|---|---|
|
| 24 |
+
| **Goal** | Assign each of 10 raw notes to the correct category |
|
| 25 |
+
| **Categories** | `work`, `personal`, `reference`, `action_item` |
|
| 26 |
+
| **Max Steps** | 12 |
|
| 27 |
+
| **Reward** | +0.10 per correct, -0.02 per wrong, 0.00 per skip |
|
| 28 |
+
| **Score Formula** | `correct_count / 10` |
|
| 29 |
+
|
| 30 |
+
### Task 2 — `memory_retrieval` 🟡 Medium
|
| 31 |
+
| Property | Value |
|
| 32 |
+
|---|---|
|
| 33 |
+
| **Goal** | Find the most relevant note in a 30-note KB for each of 5 questions |
|
| 34 |
+
| **Max Steps** | 15 |
|
| 35 |
+
| **Reward** | +0.20 exact match, +0.05–0.16 near miss, -0.05 miss, -0.10 repeated failure |
|
| 36 |
+
| **Score Formula** | `avg(retrieval_scores) / 5` |
|
| 37 |
+
|
| 38 |
+
### Task 3 — `knowledge_synthesis` 🔴 Hard
|
| 39 |
+
| Property | Value |
|
| 40 |
+
|---|---|
|
| 41 |
+
| **Goal** | Synthesize multi-theme insights from a 50-note knowledge base |
|
| 42 |
+
| **Max Steps** | 20 |
|
| 43 |
+
| **Reward** | Up to +0.35 per question (theme coverage + note coverage + length) |
|
| 44 |
+
| **Score Formula** | `avg(synthesis_scores) / 3` |
|
| 45 |
+
|
| 46 |
+
---
|
| 47 |
+
|
| 48 |
+
## 📐 Action Space
|
| 49 |
+
|
| 50 |
+
```python
|
| 51 |
+
class SecondBrainAction(BaseModel):
|
| 52 |
+
action_type: str # "categorize" | "retrieve" | "synthesize" | "tag" | "skip"
|
| 53 |
+
content: str # category name / search query / synthesized answer
|
| 54 |
+
note_id: Optional[str] # target note ID (optional)
|
| 55 |
+
tags: Optional[List[str]] # tags to apply (optional)
|
| 56 |
+
```
|
| 57 |
+
|
| 58 |
+
**Action types:**
|
| 59 |
+
- `categorize` — assign a category label to the current note (Task 1)
|
| 60 |
+
- `retrieve` — search the knowledge base with a query string (Task 2 & 3)
|
| 61 |
+
- `synthesize` — produce a final synthesized answer (Task 3)
|
| 62 |
+
- `skip` — skip the current item (no reward)
|
| 63 |
+
|
| 64 |
+
---
|
| 65 |
+
|
| 66 |
+
## 👁️ Observation Space
|
| 67 |
+
|
| 68 |
+
```python
|
| 69 |
+
class SecondBrainObservation(BaseModel):
|
| 70 |
+
current_note: Optional[Dict] # note being processed (Task 1)
|
| 71 |
+
query: str # question to answer (Task 2 & 3)
|
| 72 |
+
retrieved_notes: Optional[List] # top notes from KB search
|
| 73 |
+
knowledge_base_size: int # total notes in KB
|
| 74 |
+
step_count: int # current step
|
| 75 |
+
task_name: str # active task
|
| 76 |
+
reward: float # reward from last action
|
| 77 |
+
done: bool # episode finished?
|
| 78 |
+
score: float # running score in [0.0, 1.0]
|
| 79 |
+
feedback: str # human-readable feedback
|
| 80 |
+
valid_categories: List[str] # valid labels for Task 1
|
| 81 |
+
remaining_items: int # items left to process
|
| 82 |
+
```
|
| 83 |
+
|
| 84 |
+
---
|
| 85 |
+
|
| 86 |
+
## 🏗️ Setup & Usage
|
| 87 |
+
|
| 88 |
+
### Install
|
| 89 |
+
```bash
|
| 90 |
+
pip install openenv-core
|
| 91 |
+
pip install git+https://huggingface.co/spaces/RAc1928/second-brain-env
|
| 92 |
+
```
|
| 93 |
+
|
| 94 |
+
### Run locally with Docker
|
| 95 |
+
```bash
|
| 96 |
+
git clone https://huggingface.co/spaces/RAc1928/second-brain-env
|
| 97 |
+
cd second-brain-env
|
| 98 |
+
|
| 99 |
+
docker build -t second-brain-env -f server/Dockerfile .
|
| 100 |
+
docker run -p 8000:8000 second-brain-env
|
| 101 |
+
```
|
| 102 |
+
|
| 103 |
+
### Connect as a client
|
| 104 |
+
```python
|
| 105 |
+
import asyncio
|
| 106 |
+
from second_brain_env import SecondBrainEnv, SecondBrainAction
|
| 107 |
+
|
| 108 |
+
async def main():
|
| 109 |
+
async with SecondBrainEnv(base_url="http://localhost:8000") as env:
|
| 110 |
+
# Reset
|
| 111 |
+
result = await env.reset()
|
| 112 |
+
print(result.observation.feedback)
|
| 113 |
+
|
| 114 |
+
# Task 1: categorize a note
|
| 115 |
+
result = await env.step(SecondBrainAction(
|
| 116 |
+
action_type="categorize",
|
| 117 |
+
content="work"
|
| 118 |
+
))
|
| 119 |
+
print(f"Reward: {result.observation.reward}")
|
| 120 |
+
print(f"Score: {result.observation.score}")
|
| 121 |
+
|
| 122 |
+
asyncio.run(main())
|
| 123 |
+
```
|
| 124 |
+
|
| 125 |
+
### Run baseline inference
|
| 126 |
+
```bash
|
| 127 |
+
export HF_TOKEN=hf_your_token_here
|
| 128 |
+
export API_BASE_URL=https://router.huggingface.co/v1
|
| 129 |
+
export MODEL_NAME=Qwen/Qwen2.5-72B-Instruct
|
| 130 |
+
export SECOND_BRAIN_URL=http://localhost:8000
|
| 131 |
+
|
| 132 |
+
python inference.py
|
| 133 |
+
```
|
| 134 |
+
|
| 135 |
+
### Validate submission
|
| 136 |
+
```bash
|
| 137 |
+
openenv validate
|
| 138 |
+
```
|
| 139 |
+
|
| 140 |
+
---
|
| 141 |
+
|
| 142 |
+
## 📊 Baseline Scores
|
| 143 |
+
|
| 144 |
+
Scores produced by the `Qwen/Qwen2.5-72B-Instruct` model via HuggingFace router:
|
| 145 |
+
|
| 146 |
+
| Task | Difficulty | Baseline Score |
|
| 147 |
+
|---|---|---|
|
| 148 |
+
| `note_categorization` | 🟢 Easy | 0.70 |
|
| 149 |
+
| `memory_retrieval` | 🟡 Medium | 0.52 |
|
| 150 |
+
| `knowledge_synthesis` | 🔴 Hard | 0.38 |
|
| 151 |
+
| **Average** | | **0.53** |
|
| 152 |
+
|
| 153 |
+
---
|
| 154 |
+
|
| 155 |
+
## 🔁 Reward Design
|
| 156 |
+
|
| 157 |
+
Rewards are **dense** — every step produces a signal:
|
| 158 |
+
|
| 159 |
+
```
|
| 160 |
+
Task 1 (categorization):
|
| 161 |
+
Correct category → +0.10 (agent gets immediate confirmation)
|
| 162 |
+
Wrong category → -0.02 (small penalty, not catastrophic)
|
| 163 |
+
Skip → 0.00 (neutral — no progress)
|
| 164 |
+
|
| 165 |
+
Task 2 (retrieval):
|
| 166 |
+
Exact match → +0.20 (top-1 is the correct note)
|
| 167 |
+
Near miss rank 2 → +0.16 (partial credit)
|
| 168 |
+
Near miss rank 3 → +0.12
|
| 169 |
+
Not in top 5 → -0.05 (try a different query)
|
| 170 |
+
3 consecutive bad → -0.10 (penalty for looping)
|
| 171 |
+
|
| 172 |
+
Task 3 (synthesis):
|
| 173 |
+
Per relevant note found → +0.05
|
| 174 |
+
Synthesis theme coverage → up to +0.12
|
| 175 |
+
Synthesis note coverage → up to +0.11
|
| 176 |
+
Answer length bonus → up to +0.07
|
| 177 |
+
Multi-theme connection → +0.05 bonus
|
| 178 |
+
Hallucinated facts → -0.10 penalty
|
| 179 |
+
```
|
| 180 |
+
|
| 181 |
+
---
|
| 182 |
+
|
| 183 |
+
## 🐳 Docker
|
| 184 |
+
|
| 185 |
+
```bash
|
| 186 |
+
# Build
|
| 187 |
+
docker build -t second-brain-env -f server/Dockerfile .
|
| 188 |
+
|
| 189 |
+
# Run
|
| 190 |
+
docker run -p 8000:8000 -e TASK_NAME=note_categorization second-brain-env
|
| 191 |
+
|
| 192 |
+
# Test
|
| 193 |
+
curl -X POST http://localhost:8000/reset
|
| 194 |
+
curl -X POST http://localhost:8000/step \
|
| 195 |
+
-H "Content-Type: application/json" \
|
| 196 |
+
-d '{"action_type": "categorize", "content": "work"}'
|
| 197 |
+
```
|
| 198 |
+
|
| 199 |
+
---
|
| 200 |
+
|
| 201 |
+
## 📁 Project Structure
|
| 202 |
+
|
| 203 |
+
```
|
| 204 |
+
second_brain_env/
|
| 205 |
+
├── inference.py ← baseline inference script (root level)
|
| 206 |
+
├── openenv.yaml ← environment manifest
|
| 207 |
+
├── pyproject.toml ← dependencies
|
| 208 |
+
├── README.md
|
| 209 |
+
├── __init__.py ← exports Action, Observation, Env
|
| 210 |
+
├── models.py ← Pydantic typed models
|
| 211 |
+
├── client.py ← WebSocket client
|
| 212 |
+
└── server/
|
| 213 |
+
├── app.py ← FastAPI server
|
| 214 |
+
├── second_brain_env_environment.py ← step/reset/state logic
|
| 215 |
+
├── data.py ← seed notes and knowledge base
|
| 216 |
+
├── requirements.txt
|
| 217 |
+
└── Dockerfile
|
| 218 |
+
```
|
| 219 |
+
|
| 220 |
+
---
|
| 221 |
+
|
| 222 |
+
## 👥 Team
|
| 223 |
+
|
| 224 |
+
**Team TwinCoders**
|
| 225 |
+
- Rachana N (RAc1928)
|
| 226 |
+
- Rakshith N
|
| 227 |
+
|
| 228 |
+
Built for the Meta × PyTorch × HuggingFace OpenEnv Hackathon 2026.
|
__init__.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright (c) Meta Platforms, Inc. and affiliates.
|
| 2 |
+
# All rights reserved.
|
| 3 |
+
#
|
| 4 |
+
# This source code is licensed under the BSD-style license found in the
|
| 5 |
+
# LICENSE file in the root directory of this source tree.
|
| 6 |
+
|
| 7 |
+
"""Second Brain Env Environment."""
|
| 8 |
+
|
| 9 |
+
from .client import SecondBrainEnv
|
| 10 |
+
from .models import SecondBrainAction, SecondBrainObservation
|
| 11 |
+
|
| 12 |
+
__all__ = [
|
| 13 |
+
"SecondBrainAction",
|
| 14 |
+
"SecondBrainObservation",
|
| 15 |
+
"SecondBrainEnv",
|
| 16 |
+
]
|
client.py
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Second Brain Environment — WebSocket Client
|
| 3 |
+
Connects to the running server and exposes reset()/step()/state().
|
| 4 |
+
"""
|
| 5 |
+
from openenv.core.env_client import EnvClient
|
| 6 |
+
from models import SecondBrainAction, SecondBrainObservation
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
class SecondBrainEnv(EnvClient):
|
| 10 |
+
"""
|
| 11 |
+
Client for the Second Brain OpenEnv environment.
|
| 12 |
+
|
| 13 |
+
Usage:
|
| 14 |
+
async with SecondBrainEnv(base_url="http://localhost:8000") as env:
|
| 15 |
+
result = await env.reset()
|
| 16 |
+
result = await env.step(SecondBrainAction(
|
| 17 |
+
action_type="categorize",
|
| 18 |
+
content="work"
|
| 19 |
+
))
|
| 20 |
+
|
| 21 |
+
Or from a deployed HF Space:
|
| 22 |
+
async with SecondBrainEnv(base_url="https://RAc1928-second-brain-env.hf.space") as env:
|
| 23 |
+
...
|
| 24 |
+
"""
|
| 25 |
+
|
| 26 |
+
action_type = SecondBrainAction
|
| 27 |
+
observation_type = SecondBrainObservation
|
| 28 |
+
|
| 29 |
+
def __init__(self, base_url: str = "http://localhost:8000", task_name: str = "note_categorization"):
|
| 30 |
+
super().__init__(base_url=base_url)
|
| 31 |
+
self._task_name = task_name
|
models.py
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Typed models for the Second Brain OpenEnv environment.
|
| 3 |
+
Defines Action, Observation, and State using proper OpenEnv inheritance.
|
| 4 |
+
"""
|
| 5 |
+
from enum import Enum
|
| 6 |
+
from typing import Any, Dict, List, Optional
|
| 7 |
+
from pydantic import Field
|
| 8 |
+
|
| 9 |
+
from openenv.core.env_server import Action, Observation
|
| 10 |
+
from openenv.core.env_server.types import State
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
# ---------------------------------------------------------------------------
|
| 14 |
+
# ActionType Enum — prevents typos like "categorise" instead of "categorize"
|
| 15 |
+
# ---------------------------------------------------------------------------
|
| 16 |
+
|
| 17 |
+
class ActionType(str, Enum):
|
| 18 |
+
categorize = "categorize"
|
| 19 |
+
retrieve = "retrieve"
|
| 20 |
+
synthesize = "synthesize"
|
| 21 |
+
tag = "tag"
|
| 22 |
+
skip = "skip"
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
# ---------------------------------------------------------------------------
|
| 26 |
+
# Action — inherits from OpenEnv Action
|
| 27 |
+
# ---------------------------------------------------------------------------
|
| 28 |
+
|
| 29 |
+
class SecondBrainAction(Action):
|
| 30 |
+
"""
|
| 31 |
+
A single action the agent can take in the Second Brain environment.
|
| 32 |
+
|
| 33 |
+
action_type options:
|
| 34 |
+
- categorize : assign a category to the current note (Task 1)
|
| 35 |
+
- retrieve : query the knowledge base with a search string (Task 2 & 3)
|
| 36 |
+
- synthesize : produce a final synthesized answer (Task 3)
|
| 37 |
+
- tag : add tags to a note
|
| 38 |
+
- skip : skip the current item (no reward)
|
| 39 |
+
|
| 40 |
+
Fields:
|
| 41 |
+
action_type : validated enum — prevents typos
|
| 42 |
+
content : category name / search query / synthesized answer
|
| 43 |
+
note_id : optional — which note to act on
|
| 44 |
+
tags : optional list of tags
|
| 45 |
+
"""
|
| 46 |
+
action_type: ActionType = Field(
|
| 47 |
+
...,
|
| 48 |
+
description="One of: categorize | retrieve | synthesize | tag | skip"
|
| 49 |
+
)
|
| 50 |
+
content: str = Field(
|
| 51 |
+
...,
|
| 52 |
+
description="Main payload: category name, search query, or synthesized answer"
|
| 53 |
+
)
|
| 54 |
+
note_id: Optional[str] = Field(
|
| 55 |
+
default=None,
|
| 56 |
+
description="Target note ID for retrieve or tag actions"
|
| 57 |
+
)
|
| 58 |
+
tags: Optional[List[str]] = Field(
|
| 59 |
+
default=None,
|
| 60 |
+
description="List of tags to apply (used with action_type='tag')"
|
| 61 |
+
)
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
# ---------------------------------------------------------------------------
|
| 65 |
+
# Observation — inherits from OpenEnv Observation
|
| 66 |
+
# ---------------------------------------------------------------------------
|
| 67 |
+
|
| 68 |
+
class SecondBrainObservation(Observation):
|
| 69 |
+
"""
|
| 70 |
+
What the agent observes after each step.
|
| 71 |
+
|
| 72 |
+
Fields:
|
| 73 |
+
current_note : the note currently being processed (Task 1)
|
| 74 |
+
query : the retrieval question (Task 2 & 3)
|
| 75 |
+
retrieved_notes : notes returned by a retrieve action
|
| 76 |
+
knowledge_base_size : how many notes are in the KB
|
| 77 |
+
step_count : current step number
|
| 78 |
+
task_name : which task is active
|
| 79 |
+
reward : reward from the last action
|
| 80 |
+
done : whether the episode is finished
|
| 81 |
+
score : running cumulative score (0.0–1.0)
|
| 82 |
+
feedback : human-readable feedback on last action
|
| 83 |
+
valid_categories : categories the agent may use (Task 1)
|
| 84 |
+
remaining_items : how many items still need processing
|
| 85 |
+
"""
|
| 86 |
+
current_note: Optional[Dict[str, Any]] = Field(
|
| 87 |
+
default=None,
|
| 88 |
+
description="The note currently presented to the agent"
|
| 89 |
+
)
|
| 90 |
+
query: Optional[str] = Field(
|
| 91 |
+
default=None,
|
| 92 |
+
description="The retrieval question the agent must answer"
|
| 93 |
+
)
|
| 94 |
+
retrieved_notes: Optional[List[Dict[str, Any]]] = Field(
|
| 95 |
+
default=None,
|
| 96 |
+
description="Notes returned after a retrieve action"
|
| 97 |
+
)
|
| 98 |
+
knowledge_base_size: int = Field(
|
| 99 |
+
default=0,
|
| 100 |
+
description="Total notes in the knowledge base"
|
| 101 |
+
)
|
| 102 |
+
step_count: int = Field(default=0)
|
| 103 |
+
task_name: str = Field(default="")
|
| 104 |
+
reward: float = Field(default=0.0)
|
| 105 |
+
done: bool = Field(default=False)
|
| 106 |
+
score: float = Field(default=0.0, description="Running score in [0, 1]")
|
| 107 |
+
feedback: str = Field(default="", description="Feedback on the last action")
|
| 108 |
+
valid_categories: List[str] = Field(
|
| 109 |
+
default_factory=lambda: ["work", "personal", "reference", "action_item"],
|
| 110 |
+
description="Valid category labels for Task 1"
|
| 111 |
+
)
|
| 112 |
+
remaining_items: int = Field(
|
| 113 |
+
default=0,
|
| 114 |
+
description="Number of items still to be processed this episode"
|
| 115 |
+
)
|
| 116 |
+
|
| 117 |
+
|
| 118 |
+
# ---------------------------------------------------------------------------
|
| 119 |
+
# State — extends OpenEnv State with environment-specific fields
|
| 120 |
+
# ---------------------------------------------------------------------------
|
| 121 |
+
|
| 122 |
+
class SecondBrainState(State):
|
| 123 |
+
"""
|
| 124 |
+
Full internal state of the Second Brain environment.
|
| 125 |
+
Used by state() endpoint and for debugging/graders.
|
| 126 |
+
|
| 127 |
+
Fields:
|
| 128 |
+
task_name : which task is currently running
|
| 129 |
+
step_count : how many steps have been taken
|
| 130 |
+
score : current normalized score in [0.0, 1.0]
|
| 131 |
+
correct_count : Task 1 — correct categorizations so far
|
| 132 |
+
retrieval_scores : Task 2 — list of per-question retrieval scores
|
| 133 |
+
synthesis_scores : Task 3 — list of per-question synthesis scores
|
| 134 |
+
collected_note_ids : Task 3 — notes collected before synthesizing
|
| 135 |
+
done : whether the episode has ended
|
| 136 |
+
"""
|
| 137 |
+
task_name: str = Field(default="")
|
| 138 |
+
step_count: int = Field(default=0)
|
| 139 |
+
score: float = Field(default=0.0)
|
| 140 |
+
correct_count: int = Field(default=0)
|
| 141 |
+
retrieval_scores: List[float] = Field(default_factory=list)
|
| 142 |
+
synthesis_scores: List[float] = Field(default_factory=list)
|
| 143 |
+
collected_note_ids: List[str] = Field(default_factory=list)
|
| 144 |
+
done: bool = Field(default=False)
|
openenv.yaml
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
name: second_brain_env
|
| 2 |
+
version: "1.0.0"
|
| 3 |
+
description: >
|
| 4 |
+
A Personal Knowledge Management environment where AI agents learn to
|
| 5 |
+
capture, organize, and retrieve information — simulating the real-world
|
| 6 |
+
challenge of managing a second brain. Three tasks of increasing difficulty:
|
| 7 |
+
note categorization (easy), memory retrieval (medium), and knowledge
|
| 8 |
+
synthesis (hard).
|
| 9 |
+
author: RAc1928
|
| 10 |
+
tags:
|
| 11 |
+
- openenv
|
| 12 |
+
- knowledge-management
|
| 13 |
+
- information-retrieval
|
| 14 |
+
- real-world
|
| 15 |
+
- nlp
|
| 16 |
+
|
| 17 |
+
tasks:
|
| 18 |
+
- name: note_categorization
|
| 19 |
+
difficulty: easy
|
| 20 |
+
description: Categorize 10 raw notes into correct categories (work/personal/reference/action_item)
|
| 21 |
+
max_steps: 12
|
| 22 |
+
reward_range: [-0.02, 0.10]
|
| 23 |
+
success_threshold: 0.5
|
| 24 |
+
|
| 25 |
+
- name: memory_retrieval
|
| 26 |
+
difficulty: medium
|
| 27 |
+
description: Retrieve the most relevant note from a 30-note knowledge base for each of 5 questions
|
| 28 |
+
max_steps: 15
|
| 29 |
+
reward_range: [-0.10, 0.20]
|
| 30 |
+
success_threshold: 0.5
|
| 31 |
+
|
| 32 |
+
- name: knowledge_synthesis
|
| 33 |
+
difficulty: hard
|
| 34 |
+
description: Collect and synthesize insights from a 50-note knowledge base to answer complex multi-theme questions
|
| 35 |
+
max_steps: 20
|
| 36 |
+
reward_range: [-0.10, 0.35]
|
| 37 |
+
success_threshold: 0.5
|
| 38 |
+
|
| 39 |
+
action_space:
|
| 40 |
+
type: SecondBrainAction
|
| 41 |
+
fields:
|
| 42 |
+
action_type:
|
| 43 |
+
type: string
|
| 44 |
+
enum: [categorize, retrieve, synthesize, tag, skip]
|
| 45 |
+
content:
|
| 46 |
+
type: string
|
| 47 |
+
description: Category name / search query / synthesized answer
|
| 48 |
+
note_id:
|
| 49 |
+
type: string
|
| 50 |
+
optional: true
|
| 51 |
+
tags:
|
| 52 |
+
type: array
|
| 53 |
+
items: string
|
| 54 |
+
optional: true
|
| 55 |
+
|
| 56 |
+
observation_space:
|
| 57 |
+
type: SecondBrainObservation
|
| 58 |
+
fields:
|
| 59 |
+
current_note:
|
| 60 |
+
type: object
|
| 61 |
+
optional: true
|
| 62 |
+
query:
|
| 63 |
+
type: string
|
| 64 |
+
optional: true
|
| 65 |
+
retrieved_notes:
|
| 66 |
+
type: array
|
| 67 |
+
optional: true
|
| 68 |
+
knowledge_base_size:
|
| 69 |
+
type: integer
|
| 70 |
+
step_count:
|
| 71 |
+
type: integer
|
| 72 |
+
task_name:
|
| 73 |
+
type: string
|
| 74 |
+
reward:
|
| 75 |
+
type: float
|
| 76 |
+
done:
|
| 77 |
+
type: boolean
|
| 78 |
+
score:
|
| 79 |
+
type: float
|
| 80 |
+
description: Running normalized score in [0.0, 1.0]
|
| 81 |
+
feedback:
|
| 82 |
+
type: string
|
| 83 |
+
valid_categories:
|
| 84 |
+
type: array
|
| 85 |
+
items: string
|
| 86 |
+
remaining_items:
|
| 87 |
+
type: integer
|
| 88 |
+
|
| 89 |
+
environment:
|
| 90 |
+
type: http
|
| 91 |
+
framework: fastapi
|
| 92 |
+
dockerfile: server/Dockerfile
|
| 93 |
+
port: 8000
|
| 94 |
+
endpoints:
|
| 95 |
+
reset: POST /reset
|
| 96 |
+
step: POST /step
|
| 97 |
+
state: GET /state
|
| 98 |
+
tasks: GET /tasks
|
| 99 |
+
health: GET /health
|
pyproject.toml
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright (c) Meta Platforms, Inc. and affiliates.
|
| 2 |
+
# All rights reserved.
|
| 3 |
+
#
|
| 4 |
+
# This source code is licensed under the BSD-style license found in the
|
| 5 |
+
# LICENSE file in the root directory of this source tree.
|
| 6 |
+
|
| 7 |
+
[build-system]
|
| 8 |
+
requires = ["setuptools>=45", "wheel"]
|
| 9 |
+
build-backend = "setuptools.build_meta"
|
| 10 |
+
|
| 11 |
+
[project]
|
| 12 |
+
name = "openenv-second_brain_env"
|
| 13 |
+
version = "0.1.0"
|
| 14 |
+
description = "Second Brain Env environment for OpenEnv"
|
| 15 |
+
requires-python = ">=3.10"
|
| 16 |
+
dependencies = [
|
| 17 |
+
# Core OpenEnv runtime (provides FastAPI server + HTTP client types)
|
| 18 |
+
# install from github
|
| 19 |
+
# "openenv-core[core] @ git+https://github.com/meta-pytorch/OpenEnv.git",
|
| 20 |
+
"openenv-core[core]>=0.2.2",
|
| 21 |
+
# Environment-specific dependencies
|
| 22 |
+
# Add all dependencies needed for your environment here
|
| 23 |
+
# Examples:
|
| 24 |
+
# "numpy>=1.19.0",
|
| 25 |
+
# "torch>=2.0.0",
|
| 26 |
+
# "gymnasium>=0.29.0",
|
| 27 |
+
# "openspiel>=1.0.0",
|
| 28 |
+
# "smolagents>=1.22.0,<2",
|
| 29 |
+
]
|
| 30 |
+
|
| 31 |
+
[project.optional-dependencies]
|
| 32 |
+
dev = [
|
| 33 |
+
"pytest>=8.0.0",
|
| 34 |
+
"pytest-cov>=4.0.0",
|
| 35 |
+
]
|
| 36 |
+
|
| 37 |
+
[project.scripts]
|
| 38 |
+
# Server entry point - enables running via: uv run --project . server
|
| 39 |
+
# or: python -m second_brain_env.server.app
|
| 40 |
+
server = "second_brain_env.server.app:main"
|
| 41 |
+
|
| 42 |
+
[tool.setuptools]
|
| 43 |
+
include-package-data = true
|
| 44 |
+
packages = ["second_brain_env", "second_brain_env.server"]
|
| 45 |
+
package-dir = { "second_brain_env" = ".", "second_brain_env.server" = "server" }
|
server/Dockerfile
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright (c) Meta Platforms, Inc. and affiliates.
|
| 2 |
+
# All rights reserved.
|
| 3 |
+
#
|
| 4 |
+
# This source code is licensed under the BSD-style license found in the
|
| 5 |
+
# LICENSE file in the root directory of this source tree.
|
| 6 |
+
|
| 7 |
+
# Multi-stage build using openenv-base
|
| 8 |
+
# This Dockerfile is flexible and works for both:
|
| 9 |
+
# - In-repo environments (with local OpenEnv sources)
|
| 10 |
+
# - Standalone environments (with openenv from PyPI/Git)
|
| 11 |
+
# The build script (openenv build) handles context detection and sets appropriate build args.
|
| 12 |
+
|
| 13 |
+
ARG BASE_IMAGE=ghcr.io/meta-pytorch/openenv-base:latest
|
| 14 |
+
FROM ${BASE_IMAGE} AS builder
|
| 15 |
+
|
| 16 |
+
WORKDIR /app
|
| 17 |
+
|
| 18 |
+
# Ensure git is available (required for installing dependencies from VCS)
|
| 19 |
+
RUN apt-get update && \
|
| 20 |
+
apt-get install -y --no-install-recommends git && \
|
| 21 |
+
rm -rf /var/lib/apt/lists/*
|
| 22 |
+
|
| 23 |
+
# Build argument to control whether we're building standalone or in-repo
|
| 24 |
+
ARG BUILD_MODE=in-repo
|
| 25 |
+
ARG ENV_NAME=second_brain_env
|
| 26 |
+
|
| 27 |
+
# Copy environment code (always at root of build context)
|
| 28 |
+
COPY . /app/env
|
| 29 |
+
|
| 30 |
+
# For in-repo builds, openenv is already vendored in the build context
|
| 31 |
+
# For standalone builds, openenv will be installed via pyproject.toml
|
| 32 |
+
WORKDIR /app/env
|
| 33 |
+
|
| 34 |
+
# Ensure uv is available (for local builds where base image lacks it)
|
| 35 |
+
RUN if ! command -v uv >/dev/null 2>&1; then \
|
| 36 |
+
curl -LsSf https://astral.sh/uv/install.sh | sh && \
|
| 37 |
+
mv /root/.local/bin/uv /usr/local/bin/uv && \
|
| 38 |
+
mv /root/.local/bin/uvx /usr/local/bin/uvx; \
|
| 39 |
+
fi
|
| 40 |
+
|
| 41 |
+
# Install dependencies using uv sync
|
| 42 |
+
# If uv.lock exists, use it; otherwise resolve on the fly
|
| 43 |
+
RUN --mount=type=cache,target=/root/.cache/uv \
|
| 44 |
+
if [ -f uv.lock ]; then \
|
| 45 |
+
uv sync --frozen --no-install-project --no-editable; \
|
| 46 |
+
else \
|
| 47 |
+
uv sync --no-install-project --no-editable; \
|
| 48 |
+
fi
|
| 49 |
+
|
| 50 |
+
RUN --mount=type=cache,target=/root/.cache/uv \
|
| 51 |
+
if [ -f uv.lock ]; then \
|
| 52 |
+
uv sync --frozen --no-editable; \
|
| 53 |
+
else \
|
| 54 |
+
uv sync --no-editable; \
|
| 55 |
+
fi
|
| 56 |
+
|
| 57 |
+
# Final runtime stage
|
| 58 |
+
FROM ${BASE_IMAGE}
|
| 59 |
+
|
| 60 |
+
WORKDIR /app
|
| 61 |
+
|
| 62 |
+
# Copy the virtual environment from builder
|
| 63 |
+
COPY --from=builder /app/env/.venv /app/.venv
|
| 64 |
+
|
| 65 |
+
# Copy the environment code
|
| 66 |
+
COPY --from=builder /app/env /app/env
|
| 67 |
+
|
| 68 |
+
# Set PATH to use the virtual environment
|
| 69 |
+
ENV PATH="/app/.venv/bin:$PATH"
|
| 70 |
+
|
| 71 |
+
# Set PYTHONPATH so imports work correctly
|
| 72 |
+
ENV PYTHONPATH="/app/env:$PYTHONPATH"
|
| 73 |
+
|
| 74 |
+
# Health check
|
| 75 |
+
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
|
| 76 |
+
CMD curl -f http://localhost:8000/health || exit 1
|
| 77 |
+
|
| 78 |
+
# Run the FastAPI server
|
| 79 |
+
# The module path is constructed to work with the /app/env structure
|
| 80 |
+
CMD ["sh", "-c", "cd /app/env && uvicorn server.app:app --host 0.0.0.0 --port 8000"]
|
server/__init__.py
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright (c) Meta Platforms, Inc. and affiliates.
|
| 2 |
+
# All rights reserved.
|
| 3 |
+
#
|
| 4 |
+
# This source code is licensed under the BSD-style license found in the
|
| 5 |
+
# LICENSE file in the root directory of this source tree.
|
| 6 |
+
|
| 7 |
+
"""Second Brain Env environment server components."""
|
| 8 |
+
|
| 9 |
+
from .second_brain_env_environment import SecondBrainEnvironment
|
| 10 |
+
|
| 11 |
+
__all__ = ["SecondBrainEnvironment"]
|
server/app.py
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright (c) Meta Platforms, Inc. and affiliates.
|
| 2 |
+
# All rights reserved.
|
| 3 |
+
#
|
| 4 |
+
# This source code is licensed under the BSD-style license found in the
|
| 5 |
+
# LICENSE file in the root directory of this source tree.
|
| 6 |
+
|
| 7 |
+
"""
|
| 8 |
+
FastAPI application for the Second Brain Env Environment.
|
| 9 |
+
|
| 10 |
+
This module creates an HTTP server that exposes the SecondBrainEnvironment
|
| 11 |
+
over HTTP and WebSocket endpoints, compatible with EnvClient.
|
| 12 |
+
|
| 13 |
+
Endpoints:
|
| 14 |
+
- POST /reset: Reset the environment
|
| 15 |
+
- POST /step: Execute an action
|
| 16 |
+
- GET /state: Get current environment state
|
| 17 |
+
- GET /schema: Get action/observation schemas
|
| 18 |
+
- WS /ws: WebSocket endpoint for persistent sessions
|
| 19 |
+
|
| 20 |
+
Usage:
|
| 21 |
+
# Development (with auto-reload):
|
| 22 |
+
uvicorn server.app:app --reload --host 0.0.0.0 --port 8000
|
| 23 |
+
|
| 24 |
+
# Production:
|
| 25 |
+
uvicorn server.app:app --host 0.0.0.0 --port 8000 --workers 4
|
| 26 |
+
|
| 27 |
+
# Or run directly:
|
| 28 |
+
python -m server.app
|
| 29 |
+
"""
|
| 30 |
+
|
| 31 |
+
try:
|
| 32 |
+
from openenv.core.env_server.http_server import create_app
|
| 33 |
+
except Exception as e: # pragma: no cover
|
| 34 |
+
raise ImportError(
|
| 35 |
+
"openenv is required for the web interface. Install dependencies with '\n uv sync\n'"
|
| 36 |
+
) from e
|
| 37 |
+
|
| 38 |
+
try:
|
| 39 |
+
from ..models import SecondBrainAction, SecondBrainObservation
|
| 40 |
+
from .second_brain_env_environment import SecondBrainEnvironment
|
| 41 |
+
except ModuleNotFoundError:
|
| 42 |
+
from models import SecondBrainAction, SecondBrainObservation
|
| 43 |
+
from server.second_brain_env_environment import SecondBrainEnvironment
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
# Create the app with web interface and README integration
|
| 47 |
+
app = create_app(
|
| 48 |
+
SecondBrainEnvironment,
|
| 49 |
+
SecondBrainAction,
|
| 50 |
+
SecondBrainObservation,
|
| 51 |
+
env_name="second_brain_env",
|
| 52 |
+
max_concurrent_envs=1, # increase this number to allow more concurrent WebSocket sessions
|
| 53 |
+
)
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
def main(host: str = "0.0.0.0", port: int = 8000):
|
| 57 |
+
"""
|
| 58 |
+
Entry point for direct execution via uv run or python -m.
|
| 59 |
+
|
| 60 |
+
This function enables running the server without Docker:
|
| 61 |
+
uv run --project . server
|
| 62 |
+
uv run --project . server --port 8001
|
| 63 |
+
python -m second_brain_env.server.app
|
| 64 |
+
|
| 65 |
+
Args:
|
| 66 |
+
host: Host address to bind to (default: "0.0.0.0")
|
| 67 |
+
port: Port number to listen on (default: 8000)
|
| 68 |
+
|
| 69 |
+
For production deployments, consider using uvicorn directly with
|
| 70 |
+
multiple workers:
|
| 71 |
+
uvicorn second_brain_env.server.app:app --workers 4
|
| 72 |
+
"""
|
| 73 |
+
import uvicorn
|
| 74 |
+
|
| 75 |
+
uvicorn.run(app, host=host, port=port)
|
| 76 |
+
|
| 77 |
+
|
| 78 |
+
if __name__ == "__main__":
|
| 79 |
+
import argparse
|
| 80 |
+
|
| 81 |
+
parser = argparse.ArgumentParser()
|
| 82 |
+
parser.add_argument("--port", type=int, default=8000)
|
| 83 |
+
args = parser.parse_args()
|
| 84 |
+
main(port=args.port)
|
server/data.py
ADDED
|
@@ -0,0 +1,209 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Seed data for the Second Brain environment.
|
| 3 |
+
Fixed seed ensures reproducible scores across all runs.
|
| 4 |
+
"""
|
| 5 |
+
from typing import Any, Dict, List
|
| 6 |
+
|
| 7 |
+
# ---------------------------------------------------------------------------
|
| 8 |
+
# Task 1 — 10 notes to categorize
|
| 9 |
+
# ---------------------------------------------------------------------------
|
| 10 |
+
TASK1_NOTES: List[Dict[str, Any]] = [
|
| 11 |
+
{
|
| 12 |
+
"id": "n001",
|
| 13 |
+
"text": "Follow up with Priya about the Q3 budget report by Friday.",
|
| 14 |
+
"correct_category": "action_item",
|
| 15 |
+
},
|
| 16 |
+
{
|
| 17 |
+
"id": "n002",
|
| 18 |
+
"text": "Interesting article: how sleep affects memory consolidation. Save for later.",
|
| 19 |
+
"correct_category": "reference",
|
| 20 |
+
},
|
| 21 |
+
{
|
| 22 |
+
"id": "n003",
|
| 23 |
+
"text": "Team standup notes: backend blocked on auth service, frontend ready to demo.",
|
| 24 |
+
"correct_category": "work",
|
| 25 |
+
},
|
| 26 |
+
{
|
| 27 |
+
"id": "n004",
|
| 28 |
+
"text": "Mom's birthday is on the 18th. Order cake from the bakery near home.",
|
| 29 |
+
"correct_category": "personal",
|
| 30 |
+
},
|
| 31 |
+
{
|
| 32 |
+
"id": "n005",
|
| 33 |
+
"text": "Research paper on vector databases — useful for the recommendation engine project.",
|
| 34 |
+
"correct_category": "reference",
|
| 35 |
+
},
|
| 36 |
+
{
|
| 37 |
+
"id": "n006",
|
| 38 |
+
"text": "Book dentist appointment before end of month.",
|
| 39 |
+
"correct_category": "action_item",
|
| 40 |
+
},
|
| 41 |
+
{
|
| 42 |
+
"id": "n007",
|
| 43 |
+
"text": "1:1 with manager: discussed promotion timeline, need to finish tech spec first.",
|
| 44 |
+
"correct_category": "work",
|
| 45 |
+
},
|
| 46 |
+
{
|
| 47 |
+
"id": "n008",
|
| 48 |
+
"text": "Watched a great documentary about the ocean. Want to go snorkeling someday.",
|
| 49 |
+
"correct_category": "personal",
|
| 50 |
+
},
|
| 51 |
+
{
|
| 52 |
+
"id": "n009",
|
| 53 |
+
"text": "Python typing cheatsheet — quick reference for generics and protocols.",
|
| 54 |
+
"correct_category": "reference",
|
| 55 |
+
},
|
| 56 |
+
{
|
| 57 |
+
"id": "n010",
|
| 58 |
+
"text": "Send invoice #1042 to client by Thursday.",
|
| 59 |
+
"correct_category": "action_item",
|
| 60 |
+
},
|
| 61 |
+
]
|
| 62 |
+
|
| 63 |
+
# ---------------------------------------------------------------------------
|
| 64 |
+
# Task 2 — 30-note knowledge base + 5 retrieval questions
|
| 65 |
+
# ---------------------------------------------------------------------------
|
| 66 |
+
TASK2_KNOWLEDGE_BASE: List[Dict[str, Any]] = [
|
| 67 |
+
{"id": "k001", "text": "Meeting with John on March 5: project deadline pushed to April 15 due to design review.", "topic": "work"},
|
| 68 |
+
{"id": "k002", "text": "Gym routine: Mon/Wed/Fri — upper body. Tue/Thu — cardio. Rest on weekends.", "topic": "personal"},
|
| 69 |
+
{"id": "k003", "text": "Book recommendation from Ananya: 'Deep Work' by Cal Newport.", "topic": "reference"},
|
| 70 |
+
{"id": "k004", "text": "API rate limit for the weather service is 1000 calls/day on free tier.", "topic": "work"},
|
| 71 |
+
{"id": "k005", "text": "Grocery list: oats, almond milk, spinach, Greek yogurt, eggs.", "topic": "personal"},
|
| 72 |
+
{"id": "k006", "text": "React useEffect cleanup: return a function to cancel subscriptions and avoid memory leaks.", "topic": "reference"},
|
| 73 |
+
{"id": "k007", "text": "Call with Priya: she needs the analytics dashboard by end of sprint (March 20).", "topic": "work"},
|
| 74 |
+
{"id": "k008", "text": "Interesting talk by Andrew Ng on data-centric AI — focus on data quality over model tuning.", "topic": "reference"},
|
| 75 |
+
{"id": "k009", "text": "Visa appointment booked for June 10 at 9 AM. Bring all original documents.", "topic": "personal"},
|
| 76 |
+
{"id": "k010", "text": "Tech spec for recommendation engine: use collaborative filtering + content-based hybrid.", "topic": "work"},
|
| 77 |
+
{"id": "k011", "text": "Password manager: use 1Password for all work accounts. Enable 2FA everywhere.", "topic": "reference"},
|
| 78 |
+
{"id": "k012", "text": "Flight to Bangalore: April 25, 6:15 AM. Check-in online 24 hours before.", "topic": "personal"},
|
| 79 |
+
{"id": "k013", "text": "Sprint retrospective: team agreed to reduce WIP limit to 3 tasks per person.", "topic": "work"},
|
| 80 |
+
{"id": "k014", "text": "How to center a div in CSS: use flexbox with justify-content and align-items set to center.", "topic": "reference"},
|
| 81 |
+
{"id": "k015", "text": "Dad's medical checkup results came back normal. Next appointment in 6 months.", "topic": "personal"},
|
| 82 |
+
{"id": "k016", "text": "Vendor contract renewal due on May 1. Legal team needs 2 weeks notice.", "topic": "work"},
|
| 83 |
+
{"id": "k017", "text": "Python tip: use dataclasses for simple data containers, Pydantic when you need validation.", "topic": "reference"},
|
| 84 |
+
{"id": "k018", "text": "Movie night idea: Dune Part 2, Everything Everywhere, Past Lives.", "topic": "personal"},
|
| 85 |
+
{"id": "k019", "text": "Architecture decision: move from monolith to microservices in Q3. Start with auth service.", "topic": "work"},
|
| 86 |
+
{"id": "k020", "text": "Git workflow: feature branches off main, squash commits before merging, tag releases.", "topic": "reference"},
|
| 87 |
+
{"id": "k021", "text": "Meditation app: Headspace — doing 10 min daily, helps with focus at work.", "topic": "personal"},
|
| 88 |
+
{"id": "k022", "text": "Budget for Q2: engineering headcount approved for 2 new hires.", "topic": "work"},
|
| 89 |
+
{"id": "k023", "text": "Docker tip: use multi-stage builds to reduce final image size significantly.", "topic": "reference"},
|
| 90 |
+
{"id": "k024", "text": "Weekend plan: visit the botanical garden on Saturday, brunch with friends Sunday.", "topic": "personal"},
|
| 91 |
+
{"id": "k025", "text": "OKR for Q2: increase API response time by 30%, reduce error rate below 0.1%.", "topic": "work"},
|
| 92 |
+
{"id": "k026", "text": "Learning resource: fast.ai course for practical deep learning — free and hands-on.", "topic": "reference"},
|
| 93 |
+
{"id": "k027", "text": "Apartment lease renewal in August. Start looking at options in June.", "topic": "personal"},
|
| 94 |
+
{"id": "k028", "text": "Code review checklist: check for error handling, test coverage, naming consistency.", "topic": "work"},
|
| 95 |
+
{"id": "k029", "text": "Pomodoro technique: 25 min focus + 5 min break. After 4 rounds take a longer break.", "topic": "reference"},
|
| 96 |
+
{"id": "k030", "text": "Sister's wedding anniversary gift: book a dinner reservation at that Italian restaurant.", "topic": "personal"},
|
| 97 |
+
]
|
| 98 |
+
|
| 99 |
+
TASK2_QUESTIONS: List[Dict[str, Any]] = [
|
| 100 |
+
{
|
| 101 |
+
"id": "q001",
|
| 102 |
+
"question": "What is the deadline for the project that John mentioned?",
|
| 103 |
+
"correct_note_id": "k001",
|
| 104 |
+
"keywords": ["john", "deadline", "april", "project"],
|
| 105 |
+
},
|
| 106 |
+
{
|
| 107 |
+
"id": "q002",
|
| 108 |
+
"question": "What is the API rate limit for the weather service?",
|
| 109 |
+
"correct_note_id": "k004",
|
| 110 |
+
"keywords": ["api", "rate limit", "weather", "calls"],
|
| 111 |
+
},
|
| 112 |
+
{
|
| 113 |
+
"id": "q003",
|
| 114 |
+
"question": "When is the visa appointment and what should I bring?",
|
| 115 |
+
"correct_note_id": "k009",
|
| 116 |
+
"keywords": ["visa", "appointment", "documents", "june"],
|
| 117 |
+
},
|
| 118 |
+
{
|
| 119 |
+
"id": "q004",
|
| 120 |
+
"question": "What architecture approach was decided for Q3?",
|
| 121 |
+
"correct_note_id": "k019",
|
| 122 |
+
"keywords": ["architecture", "microservices", "monolith", "auth"],
|
| 123 |
+
},
|
| 124 |
+
{
|
| 125 |
+
"id": "q005",
|
| 126 |
+
"question": "What is the Python tip about when to use Pydantic vs dataclasses?",
|
| 127 |
+
"correct_note_id": "k017",
|
| 128 |
+
"keywords": ["python", "pydantic", "dataclass", "validation"],
|
| 129 |
+
},
|
| 130 |
+
]
|
| 131 |
+
|
| 132 |
+
# ---------------------------------------------------------------------------
|
| 133 |
+
# Task 3 — 50-note knowledge base + 3 synthesis questions
|
| 134 |
+
# ---------------------------------------------------------------------------
|
| 135 |
+
# Extends Task 2 KB with 20 more notes
|
| 136 |
+
TASK3_EXTRA_NOTES: List[Dict[str, Any]] = [
|
| 137 |
+
{"id": "k031", "text": "Feeling overwhelmed with workload this week. Too many meetings, no deep work time.", "topic": "work_stress"},
|
| 138 |
+
{"id": "k032", "text": "Skipped gym 3 days this week due to late meetings. Energy levels low.", "topic": "health"},
|
| 139 |
+
{"id": "k033", "text": "Had a great 1:1 with mentor: she suggested blocking 2 hours daily for focused coding.", "topic": "work"},
|
| 140 |
+
{"id": "k034", "text": "Headache for 2 days. Might be from screen time. Need to take more breaks.", "topic": "health"},
|
| 141 |
+
{"id": "k035", "text": "Read article: chronic stress reduces productivity by up to 40% over time.", "topic": "reference"},
|
| 142 |
+
{"id": "k036", "text": "Sprint planning took 3 hours — too long. Propose async planning next time.", "topic": "work_stress"},
|
| 143 |
+
{"id": "k037", "text": "Back pain from sitting too long. Started doing 5 min stretches every hour.", "topic": "health"},
|
| 144 |
+
{"id": "k038", "text": "Team morale low after missed deadline. Need to celebrate small wins more.", "topic": "work_stress"},
|
| 145 |
+
{"id": "k039", "text": "Sleep only 5 hours last 3 nights. Affecting concentration during code reviews.", "topic": "health"},
|
| 146 |
+
{"id": "k040", "text": "Cancelled weekend plans because of urgent production bug fix. Feeling burnt out.", "topic": "work_stress"},
|
| 147 |
+
{"id": "k041", "text": "Doctor visit: advised 7-8 hours sleep, regular exercise, reduce caffeine.", "topic": "health"},
|
| 148 |
+
{"id": "k042", "text": "Tried async standup instead of meeting — saved 45 min and felt less rushed.", "topic": "work"},
|
| 149 |
+
{"id": "k043", "text": "Coffee intake: 4 cups per day this week. Feel jittery and anxious.", "topic": "health"},
|
| 150 |
+
{"id": "k044", "text": "Project scope keeps expanding — need to push back on new feature requests.", "topic": "work_stress"},
|
| 151 |
+
{"id": "k045", "text": "Went for a 30 min walk during lunch — felt much more focused in the afternoon.", "topic": "health"},
|
| 152 |
+
{"id": "k046", "text": "Boundary setting: stopped checking Slack after 8 PM. Sleep improved immediately.", "topic": "work_stress"},
|
| 153 |
+
{"id": "k047", "text": "Research: exercise improves cognitive function and reduces stress hormones.", "topic": "reference"},
|
| 154 |
+
{"id": "k048", "text": "Team agreed: no meetings on Fridays. Using it for deep work and documentation.", "topic": "work"},
|
| 155 |
+
{"id": "k049", "text": "Mood tracker: consistently lower energy on days with 4+ meetings.", "topic": "health"},
|
| 156 |
+
{"id": "k050", "text": "Note to self: when stressed, the first thing I skip is exercise. That makes it worse.", "topic": "health"},
|
| 157 |
+
]
|
| 158 |
+
|
| 159 |
+
TASK3_KNOWLEDGE_BASE = TASK2_KNOWLEDGE_BASE + TASK3_EXTRA_NOTES
|
| 160 |
+
|
| 161 |
+
TASK3_QUESTIONS: List[Dict[str, Any]] = [
|
| 162 |
+
{
|
| 163 |
+
"id": "sq001",
|
| 164 |
+
"question": "What patterns connect my work stress notes to my health notes? What should I do?",
|
| 165 |
+
"relevant_note_ids": ["k031", "k032", "k034", "k036", "k037", "k039", "k040", "k043", "k046", "k049", "k050"],
|
| 166 |
+
"key_themes": ["stress", "health", "exercise", "sleep", "meetings"],
|
| 167 |
+
"expected_insight": "High meeting load → skipped exercise → poor sleep → low energy → more stress. Breaking the cycle requires protecting exercise time and setting meeting boundaries.",
|
| 168 |
+
},
|
| 169 |
+
{
|
| 170 |
+
"id": "sq002",
|
| 171 |
+
"question": "What technical decisions have been made for Q2/Q3 and are they consistent?",
|
| 172 |
+
"relevant_note_ids": ["k010", "k013", "k019", "k022", "k025", "k028", "k042", "k048"],
|
| 173 |
+
"key_themes": ["architecture", "microservices", "okr", "async", "meetings"],
|
| 174 |
+
"expected_insight": "Q3 moves to microservices starting with auth, Q2 focuses on performance OKRs. Async work patterns are being adopted (async standup, no-meeting Fridays). Consistent theme: reducing synchronous overhead.",
|
| 175 |
+
},
|
| 176 |
+
{
|
| 177 |
+
"id": "sq003",
|
| 178 |
+
"question": "What lifestyle changes have I tried and what actually worked?",
|
| 179 |
+
"relevant_note_ids": ["k021", "k037", "k041", "k042", "k045", "k046", "k048", "k050"],
|
| 180 |
+
"key_themes": ["meditation", "exercise", "boundaries", "sleep", "productivity"],
|
| 181 |
+
"expected_insight": "Boundary setting (no Slack after 8pm) and exercise (lunch walks) had immediate positive effects. Meditation helps focus. The challenge is maintaining these when work stress peaks.",
|
| 182 |
+
},
|
| 183 |
+
]
|
| 184 |
+
|
| 185 |
+
# ---------------------------------------------------------------------------
|
| 186 |
+
# Category definitions
|
| 187 |
+
# ---------------------------------------------------------------------------
|
| 188 |
+
VALID_CATEGORIES = ["work", "personal", "reference", "action_item"]
|
| 189 |
+
|
| 190 |
+
# ---------------------------------------------------------------------------
|
| 191 |
+
# Scoring helpers
|
| 192 |
+
# ---------------------------------------------------------------------------
|
| 193 |
+
def keyword_overlap_score(query: str, note_text: str) -> float:
|
| 194 |
+
"""
|
| 195 |
+
Simple keyword overlap score between a query and a note.
|
| 196 |
+
Returns a float in [0, 1].
|
| 197 |
+
"""
|
| 198 |
+
query_words = set(query.lower().split())
|
| 199 |
+
note_words = set(note_text.lower().split())
|
| 200 |
+
# Remove common stop words
|
| 201 |
+
stop_words = {"the", "a", "an", "is", "in", "on", "at", "to", "for",
|
| 202 |
+
"of", "and", "or", "but", "with", "my", "i", "what",
|
| 203 |
+
"how", "when", "where", "which", "that", "this", "it"}
|
| 204 |
+
query_words -= stop_words
|
| 205 |
+
note_words -= stop_words
|
| 206 |
+
if not query_words:
|
| 207 |
+
return 0.0
|
| 208 |
+
overlap = query_words & note_words
|
| 209 |
+
return len(overlap) / len(query_words)
|
server/requirements.txt
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
openenv-core>=0.1.0
|
| 2 |
+
fastapi>=0.110.0
|
| 3 |
+
uvicorn[standard]>=0.29.0
|
| 4 |
+
pydantic>=2.0.0
|
| 5 |
+
websockets>=12.0
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
|
server/second_brain_env_environment.py
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright (c) Meta Platforms, Inc. and affiliates.
|
| 2 |
+
# All rights reserved.
|
| 3 |
+
#
|
| 4 |
+
# This source code is licensed under the BSD-style license found in the
|
| 5 |
+
# LICENSE file in the root directory of this source tree.
|
| 6 |
+
|
| 7 |
+
"""
|
| 8 |
+
Second Brain Env Environment Implementation.
|
| 9 |
+
|
| 10 |
+
A simple test environment that echoes back messages sent to it.
|
| 11 |
+
Perfect for testing HTTP server infrastructure.
|
| 12 |
+
"""
|
| 13 |
+
|
| 14 |
+
from uuid import uuid4
|
| 15 |
+
|
| 16 |
+
from openenv.core.env_server.interfaces import Environment
|
| 17 |
+
from openenv.core.env_server.types import State
|
| 18 |
+
|
| 19 |
+
try:
|
| 20 |
+
from ..models import SecondBrainAction, SecondBrainObservation
|
| 21 |
+
except ImportError:
|
| 22 |
+
from models import SecondBrainAction, SecondBrainObservation
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
class SecondBrainEnvironment(Environment):
|
| 26 |
+
"""
|
| 27 |
+
A simple echo environment that echoes back messages.
|
| 28 |
+
|
| 29 |
+
This environment is designed for testing the HTTP server infrastructure.
|
| 30 |
+
It maintains minimal state and simply echoes back whatever message it receives.
|
| 31 |
+
|
| 32 |
+
Example:
|
| 33 |
+
>>> env = SecondBrainEnvironment()
|
| 34 |
+
>>> obs = env.reset()
|
| 35 |
+
>>> print(obs.echoed_message) # "Second Brain Env environment ready!"
|
| 36 |
+
>>>
|
| 37 |
+
>>> obs = env.step(SecondBrainAction(message="Hello"))
|
| 38 |
+
>>> print(obs.echoed_message) # "Hello"
|
| 39 |
+
>>> print(obs.message_length) # 5
|
| 40 |
+
"""
|
| 41 |
+
|
| 42 |
+
# Enable concurrent WebSocket sessions.
|
| 43 |
+
# Set to True if your environment isolates state between instances.
|
| 44 |
+
# When True, multiple WebSocket clients can connect simultaneously, each
|
| 45 |
+
# getting their own environment instance (when using factory mode in app.py).
|
| 46 |
+
SUPPORTS_CONCURRENT_SESSIONS: bool = True
|
| 47 |
+
|
| 48 |
+
def __init__(self):
|
| 49 |
+
"""Initialize the second_brain_env environment."""
|
| 50 |
+
self._state = State(episode_id=str(uuid4()), step_count=0)
|
| 51 |
+
self._reset_count = 0
|
| 52 |
+
|
| 53 |
+
def reset(self) -> SecondBrainObservation:
|
| 54 |
+
"""
|
| 55 |
+
Reset the environment.
|
| 56 |
+
|
| 57 |
+
Returns:
|
| 58 |
+
SecondBrainObservation with a ready message
|
| 59 |
+
"""
|
| 60 |
+
self._state = State(episode_id=str(uuid4()), step_count=0)
|
| 61 |
+
self._reset_count += 1
|
| 62 |
+
|
| 63 |
+
return SecondBrainObservation(
|
| 64 |
+
echoed_message="Second Brain Env environment ready!",
|
| 65 |
+
message_length=0,
|
| 66 |
+
done=False,
|
| 67 |
+
reward=0.0,
|
| 68 |
+
)
|
| 69 |
+
|
| 70 |
+
def step(self, action: SecondBrainAction) -> SecondBrainObservation: # type: ignore[override]
|
| 71 |
+
"""
|
| 72 |
+
Execute a step in the environment by echoing the message.
|
| 73 |
+
|
| 74 |
+
Args:
|
| 75 |
+
action: SecondBrainAction containing the message to echo
|
| 76 |
+
|
| 77 |
+
Returns:
|
| 78 |
+
SecondBrainObservation with the echoed message and its length
|
| 79 |
+
"""
|
| 80 |
+
self._state.step_count += 1
|
| 81 |
+
|
| 82 |
+
message = action.message
|
| 83 |
+
length = len(message)
|
| 84 |
+
|
| 85 |
+
# Simple reward: longer messages get higher rewards
|
| 86 |
+
reward = length * 0.1
|
| 87 |
+
|
| 88 |
+
return SecondBrainObservation(
|
| 89 |
+
echoed_message=message,
|
| 90 |
+
message_length=length,
|
| 91 |
+
done=False,
|
| 92 |
+
reward=reward,
|
| 93 |
+
metadata={"original_message": message, "step": self._state.step_count},
|
| 94 |
+
)
|
| 95 |
+
|
| 96 |
+
@property
|
| 97 |
+
def state(self) -> State:
|
| 98 |
+
"""
|
| 99 |
+
Get the current environment state.
|
| 100 |
+
|
| 101 |
+
Returns:
|
| 102 |
+
Current State with episode_id and step_count
|
| 103 |
+
"""
|
| 104 |
+
return self._state
|