Rakshithn123 commited on
Commit
ea1db8d
·
0 Parent(s):
.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
+ [![OpenEnv](https://img.shields.io/badge/OpenEnv-compatible-blue)](https://github.com/meta-pytorch/OpenEnv)
6
+ [![HuggingFace](https://img.shields.io/badge/🤗-HuggingFace%20Space-yellow)](https://huggingface.co/spaces/RAc1928/second-brain-env)
7
+ [![Python](https://img.shields.io/badge/Python-3.10%2B-green)](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