Skip to content

Commit 202a216

Browse files
penny-team[bot]jaredlockhartclaude
authored
feat: /learn command and adaptive learn loop agent (jaredlockhart#290) (jaredlockhart#303)
Add search-based /learn command for entity discovery and LearnLoopAgent for adaptive background research driven by interest scores. /learn <topic> searches via Perplexity, discovers entities from results via LLM entity identification, and seeds them with LEARN_COMMAND engagements. Works for both specific entities (/learn kef ls50) and broad topics (/learn travel in china 2026). The learn loop then continues researching those entities in the background, prioritized by interest score, fact count, and staleness. Co-authored-by: Jared Lockhart <119884+jaredlockhart@users.noreply.github.com> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent a6a9fae commit 202a216

15 files changed

Lines changed: 1234 additions & 8 deletions

File tree

CLAUDE.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,7 @@ GitHub Actions runs `make check` (format, lint, typecheck, tests) on every push
134134
- `FOLLOWUP_MAX_SECONDS`: Maximum random delay after idle for followup (default: 7200)
135135
- `DISCOVERY_MIN_SECONDS`: Minimum random delay after idle for discovery (default: 7200)
136136
- `DISCOVERY_MAX_SECONDS`: Maximum random delay after idle for discovery (default: 14400)
137+
- `LEARN_LOOP_INTERVAL`: Interval for learn loop in seconds, runs during idle (default: 300, runtime-configurable)
137138
- `RESEARCH_MAX_ITERATIONS`: Max search iterations per research task (default: 10, runtime-configurable)
138139
- `RESEARCH_OUTPUT_MAX_LENGTH`: Max research report length in characters (default: 2000, runtime-configurable)
139140
- `TOOL_TIMEOUT`: Tool execution timeout in seconds (default: 60)

penny/CLAUDE.md

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ flowchart TD
1616
Channel -->|"8. reply + image"| User
1717
1818
Penny -.->|"log"| DB[(SQLite)]
19-
Penny -.->|"schedule"| BG["Background Agents\nResearch · Followup · Extraction · Discovery · EntityCleaner"]
19+
Penny -.->|"schedule"| BG["Background Agents\nResearch · Followup · Extraction · Discovery · EntityCleaner · LearnLoop"]
2020
```
2121

2222
- **Channels**: Signal (WebSocket + REST) or Discord (discord.py bot)
@@ -46,6 +46,7 @@ penny/
4646
discovery.py — DiscoveryAgent: proactive content sharing based on user interests
4747
research.py — ResearchAgent: autonomous multi-iteration deep research
4848
entity_cleaner.py — EntityCleaner: periodic merge of duplicate entities
49+
learn_loop.py — LearnLoopAgent: adaptive background research driven by interest scores
4950
scheduler/
5051
base.py — BackgroundScheduler + Schedule ABC
5152
schedules.py — PeriodicSchedule, DelayedSchedule, AlwaysRunSchedule implementations
@@ -58,6 +59,7 @@ penny/
5859
debug.py — /debug: show agent status, git commit, system info
5960
index.py — /commands: list available commands
6061
profile.py — /profile: user info collection (name, location, DOB, timezone)
62+
learn.py — /learn: signal active research interest in a topic
6163
preferences.py — /like, /dislike, /unlike, /undislike: explicit preference management
6264
personality.py — /personality: customize Penny's tone and behavior per user
6365
research.py — /research: start autonomous deep research tasks
@@ -98,13 +100,13 @@ penny/
98100
ollama_patches.py — Ollama SDK monkeypatch (MockOllamaAsyncClient)
99101
search_patches.py — Perplexity + DuckDuckGo SDK monkeypatches
100102
agents/ — Per-agent integration tests
101-
test_message.py, test_followup.py, test_extraction.py, test_discovery.py, test_research.py, test_entity_cleaner.py
103+
test_message.py, test_followup.py, test_extraction.py, test_discovery.py, test_research.py, test_entity_cleaner.py, test_learn_loop.py
102104
channels/ — Channel integration tests
103105
test_signal_channel.py, test_signal_reactions.py, test_signal_vision.py, test_startup_announcement.py
104106
commands/ — Per-command tests
105107
test_commands.py, test_debug.py, test_config.py, test_draw.py, test_email.py,
106-
test_preferences.py, test_personality.py, test_research.py, test_schedule.py,
107-
test_bug.py, test_system.py, test_test_mode.py
108+
test_learn.py, test_preferences.py, test_personality.py, test_research.py,
109+
test_schedule.py, test_bug.py, test_system.py, test_test_mode.py
108110
database/ — Migration validation tests
109111
test_migrations.py
110112
jmap/ — JMAP client tests
@@ -168,6 +170,20 @@ The base `Agent` class implements the core agentic loop:
168170
- Merge executed in Python-space: combines deduplicated facts, reassigns entity_search_log refs, deletes duplicates
169171
- Uses Ollama structured output with Pydantic schemas (`MergeGroups`, `MergeGroup`)
170172

173+
**LearnLoopAgent** (`agents/learn_loop.py`)
174+
- Background task: adaptive research driven by entity interest scores
175+
- Picks the highest-priority entity across all users each cycle
176+
- Priority scoring: `interest × (1/fact_count) × staleness_factor` (Python-space, no LLM)
177+
- Two modes: **enrichment** (< 5 facts, broad search) and **briefing** (5+ facts, novelty check)
178+
- Skips entities with negative interest or recently verified facts (< 1 day)
179+
- Uses SearchTool directly (not the agentic loop) for Perplexity searches
180+
- Extracts facts via `ollama_client.generate()` with structured output (Pydantic schema)
181+
- Two-pass fact dedup: normalized string match (fast) then embedding similarity (threshold 0.85)
182+
- Confirms existing facts by updating `last_verified` timestamps
183+
- Composes casual message about novel findings and sends via channel
184+
- Triggered by `/learn` command (creates LEARN_COMMAND engagement with high strength)
185+
- Scheduled as `PeriodicSchedule` (idle-only, default 300s interval)
186+
171187
**ResearchAgent** (`agents/research.py`)
172188
- Background task: autonomous multi-iteration deep research on user-requested topics
173189
- Manages `ResearchTask` and `ResearchIteration` database records
@@ -190,7 +206,7 @@ The base `Agent` class implements the core agentic loop:
190206
The `scheduler/` module manages background tasks:
191207

192208
### BackgroundScheduler (`scheduler/base.py`)
193-
- Runs tasks in priority order (schedule → research → extraction → entity_cleaner → followup → discovery)
209+
- Runs tasks in priority order (schedule → research → extraction → entity_cleaner → learn_loop → followup → discovery)
194210
- Tracks global idle threshold (default: 300s)
195211
- Notifies schedules when messages arrive (resets timers)
196212
- Only runs one task per tick
@@ -251,6 +267,7 @@ Penny supports slash commands sent as messages (e.g., `/debug`, `/config`). Comm
251267
- **/debug** (`debug.py`): Shows agent status, git commit, system info, background task state
252268
- **/config** (`config.py`): View and modify runtime settings (e.g., `/config idle_seconds 600`)
253269
- **/profile** (`profile.py`): View or update user profile (name, location, DOB). Derives IANA timezone from location. Required before Penny will chat
270+
- **/learn** (`learn.py`): Signal active interest in a topic for background research. `/learn` lists tracked entities; `/learn <topic>` searches via SearchTool, discovers entities from results via LLM entity identification, creates them with LEARN_COMMAND engagements. Works for both specific entities (`/learn kef ls50`) and broad topics (`/learn travel in china 2026`). Falls back to creating a single entity from topic text if no SearchTool is configured
254271
- **/like**, **/dislike**, **/unlike**, **/undislike** (`preferences.py`): Explicitly manage preferences for discovery
255272
- **/personality** (`personality.py`): View, set, or reset custom personality prompt per user. Affects Penny's tone and behavior
256273
- **/research** (`research.py`): Start autonomous multi-iteration research. Supports clarification step with output format options, task queueing, and cancellation
@@ -266,7 +283,7 @@ Penny supports slash commands sent as messages (e.g., `/debug`, `/config`). Comm
266283
- `/config` reads and writes to a `RuntimeConfig` table in SQLite (migration `0002_add_runtime_config_table.py`)
267284
- `ConfigParam` definitions in `config_params.py` declare which settings are runtime-configurable, with types and validation
268285
- Config values are read on each use (not cached), so changes take effect immediately
269-
- Configurable params: `MESSAGE_MAX_STEPS`, `IDLE_SECONDS`, `FOLLOWUP_MIN/MAX_SECONDS`, `DISCOVERY_MIN/MAX_SECONDS`, `MAINTENANCE_INTERVAL_SECONDS`, `RESEARCH_MAX_ITERATIONS`, `RESEARCH_OUTPUT_MAX_LENGTH`
286+
- Configurable params: `MESSAGE_MAX_STEPS`, `IDLE_SECONDS`, `FOLLOWUP_MIN/MAX_SECONDS`, `DISCOVERY_MIN/MAX_SECONDS`, `MAINTENANCE_INTERVAL_SECONDS`, `RESEARCH_MAX_ITERATIONS`, `RESEARCH_OUTPUT_MAX_LENGTH`, `LEARN_LOOP_INTERVAL`
270287

271288
## Message Flow
272289

@@ -300,7 +317,7 @@ Penny supports slash commands sent as messages (e.g., `/debug`, `/config`). Comm
300317
- **Duplicate tool blocking**: Agent tracks called tools per message to prevent LLM tool-call loops
301318
- **Tool parameter validation**: Tool parameters validated before execution; non-existent tools return clear error messages
302319
- **Specialized agents**: Each task type (message, research, followup, preference, discovery) has its own agent subclass
303-
- **Priority scheduling**: Schedule → research → extraction → entity_cleaner → followup → discovery
320+
- **Priority scheduling**: Schedule → research → extraction → entity_cleaner → learn_loop → followup → discovery
304321
- **Always-run schedules**: Research and user-created schedules run regardless of idle state; extraction/followup/discovery wait for idle
305322
- **Global idle threshold**: Single configurable idle time (default: 300s) controls when idle-dependent tasks become eligible
306323
- **Background suspension**: Foreground message processing suspends background tasks to prevent interference

penny/penny/agents/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from penny.agents.discovery import DiscoveryAgent
55
from penny.agents.extraction import ExtractionPipeline
66
from penny.agents.followup import FollowupAgent
7+
from penny.agents.learn_loop import LearnLoopAgent
78
from penny.agents.message import MessageAgent
89
from penny.agents.models import (
910
ChatMessage,
@@ -19,6 +20,7 @@
1920
"DiscoveryAgent",
2021
"ExtractionPipeline",
2122
"FollowupAgent",
23+
"LearnLoopAgent",
2224
"MessageAgent",
2325
"MessageRole",
2426
"ResearchAgent",

0 commit comments

Comments
 (0)