Skip to content

Commit 617f376

Browse files
v1.3.0: Agent-based mentions + Auto-discovery tools
Agent-based mention handling: - 3 LLM calls per mention (select -> plan -> reply) - Multi-mention selection with priority - Per-mention tool planning - User conversation history for context - Tools used tracking Auto-discovery tool system: - Tools auto-discovered from tools/ directory via pkgutil - TOOL_SCHEMA constant for automatic registration - No manual registry updates needed Tool updates: - web_search.py: Added TOOL_SCHEMA - image_generation.py: TOOL_SCHEMA, jfif support, ALL reference images New files: - config/prompts/mention_selector_agent.py - config/prompts/mention_reply_agent.py Infrastructure: - TierManager accepts database for fallback storage - Renamed AGENT_PROMPT_TEMPLATE -> AUTOPOST_AGENT_PROMPT
1 parent b0d5bdf commit 617f376

File tree

14 files changed

+988
-296
lines changed

14 files changed

+988
-296
lines changed

ARCHITECTURE.md

Lines changed: 145 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -149,15 +149,31 @@ LLM prompts for specific services:
149149
- Contains `{tools_desc}` placeholder for dynamic tool injection
150150

151151
**`mention_selector.py`**`MENTION_SELECTOR_PROMPT`
152-
- Instructions for selecting and responding to mentions
152+
- Legacy mention selector (v1.2)
153153
- Criteria for what to reply to and what to ignore
154154

155+
**`mention_selector_agent.py`**`MENTION_SELECTOR_AGENT_PROMPT` (v1.3)
156+
- Instructions for selecting multiple mentions worth replying to
157+
- Returns array of selected mentions with priority
158+
159+
**`mention_reply_agent.py`**`MENTION_REPLY_AGENT_PROMPT` (v1.3)
160+
- Instructions for planning and generating individual replies
161+
- Contains `{tools_desc}` placeholder for dynamic tool injection
162+
155163
### config/schemas.py
156164
JSON schemas for structured LLM output:
157165

158-
**`PLAN_SCHEMA`** — Agent plan format `{reasoning, plan: [{tool, params}]}`
159-
**`POST_TEXT_SCHEMA`** — Final tweet format `{post_text}`
160-
**`MENTION_SELECTOR_SCHEMA`** — Mention response format `{selected_tweet_id, text, include_picture, reasoning}`
166+
**Autopost schemas:**
167+
- **`PLAN_SCHEMA`** — Agent plan format `{reasoning, plan: [{tool, params}]}`
168+
- **`POST_TEXT_SCHEMA`** — Final tweet format `{post_text}`
169+
170+
**Legacy mention schema (v1.2):**
171+
- **`MENTION_SELECTOR_SCHEMA`** — Single mention response `{selected_tweet_id, text, include_picture, reasoning}`
172+
173+
**Agent-based mention schemas (v1.3):**
174+
- **`MENTION_SELECTION_SCHEMA`** — Array of selected mentions `{selected_mentions: [{tweet_id, priority, reasoning, suggested_approach}]}`
175+
- **`MENTION_PLAN_SCHEMA`** — Reply plan format `{reasoning, plan: [{tool, params}]}`
176+
- **`REPLY_TEXT_SCHEMA`** — Final reply format `{reply_text}`
161177

162178
### services/autopost.py
163179
`AutoPostService` class — agent-based autoposting with tool execution.
@@ -194,29 +210,56 @@ The agent operates in a continuous conversation, creating a plan and executing t
194210
- Dynamic tool discovery via `get_tools_description()` from registry
195211

196212
### services/mentions.py
197-
`MentionHandler` class processes Twitter mentions.
213+
`MentionAgentHandler` class — agent-based mention processing (v1.3).
214+
215+
**Agent Architecture (3 LLM calls per mention):**
198216

199-
Flow:
217+
**Flow:**
200218
1. Fetches recent mentions from Twitter API
201219
2. Filters out already-processed mentions using database
202-
3. Sends all unprocessed mentions to LLM in single request
203-
4. LLM selects ONE mention to reply to (or none if all low quality)
204-
5. LLM returns selection + reply text + image decision
205-
6. If mention selected, optionally generates image
206-
7. Posts reply to Twitter
207-
8. Saves interaction to database
208-
209-
Structured output schema:
220+
3. Optional: Filters by whitelist (for testing)
221+
4. **LLM Call #1:** Select mentions worth replying to (returns array)
222+
5. For EACH selected mention:
223+
a. Get user conversation history from database
224+
b. **LLM Call #2:** Create plan `{reasoning, plan: [{tool, params}]}`
225+
c. Execute tools (web_search, generate_image)
226+
d. **LLM Call #3:** Generate final reply text
227+
e. Upload image if generated
228+
f. Post reply to Twitter
229+
g. Save to database with tools_used tracking
230+
6. Return batch summary
231+
232+
**Selection Schema (LLM #1):**
233+
```json
234+
{
235+
"selected_mentions": [
236+
{
237+
"tweet_id": "123456",
238+
"priority": 1,
239+
"reasoning": "Why this mention is worth replying to",
240+
"suggested_approach": "Brief hint for how to reply"
241+
}
242+
]
243+
}
244+
```
245+
246+
**Plan Schema (LLM #2):**
210247
```json
211248
{
212-
"selected_tweet_id": "string (tweet ID to reply to, empty if none)",
213-
"text": "string (reply text, max 280 chars)",
214-
"include_picture": "boolean",
215-
"reasoning": "string (why this mention was selected)"
249+
"reasoning": "How to approach this reply",
250+
"plan": [
251+
{"tool": "web_search", "params": {"query": "..."}},
252+
{"tool": "generate_image", "params": {"prompt": "..."}}
253+
]
216254
}
217255
```
218256

219-
**Design decision:** LLM evaluates ALL pending mentions and picks the best one, rather than replying to every mention. This creates more authentic engagement.
257+
**Design decisions:**
258+
- LLM selects MULTIPLE mentions (with priority) instead of just one
259+
- Each mention gets individual planning and tool execution
260+
- User conversation history provides context for personalized replies
261+
- Empty plan `[]` is valid — most replies don't need tools
262+
- Tracks which tools were used for analytics
220263

221264
### services/llm.py
222265
`LLMClient` class — async client for OpenRouter API.
@@ -302,27 +345,55 @@ TIER_FEATURES = {
302345
- Both services receive `tier_manager` instance via constructor
303346

304347
### tools/registry.py
305-
Tool registry for agent function calling. Contains:
306-
- `TOOLS` — dict mapping tool names to async functions
307-
- `TOOLS_SCHEMA` — list of JSON schemas in OpenAI function calling format
348+
Tool registry with **auto-discovery** (v1.3).
349+
350+
**How it works:**
351+
- Uses `pkgutil.iter_modules()` to scan all Python files in `tools/` directory
352+
- Each tool file that exports `TOOL_SCHEMA` is automatically registered
353+
- Tool function must have the same name as `schema["function"]["name"]`
354+
355+
**Exports:**
356+
- `TOOLS` — dict mapping tool names to async functions (auto-populated)
357+
- `TOOLS_SCHEMA` — list of JSON schemas in OpenAI function calling format (auto-populated)
308358
- `get_tools_description()` — generates human-readable tool descriptions for agent prompts
359+
- `refresh_tools()` — re-scan tools at runtime
309360

310361
**Available tools:**
311362
- `web_search` — real-time web search via OpenRouter plugins
312363
- `generate_image` — image generation using Gemini 3 Pro
313364

314-
**Dynamic tool discovery:**
315-
The `get_tools_description()` function automatically generates tool documentation from `TOOLS_SCHEMA`. When you add a new tool, it appears in the agent's system prompt automatically.
365+
**To add a new tool (zero registry changes needed):**
366+
1. Create `tools/my_tool.py`
367+
2. Add `TOOL_SCHEMA` constant with OpenAI function calling format
368+
3. Create async function with matching name
369+
4. Done! Tool is auto-discovered on startup
316370

317-
To add a new tool:
318-
1. Create tool function in `tools/` directory
319-
2. Import and add to `TOOLS` dict
320-
3. Add JSON schema to `TOOLS_SCHEMA` list
321-
4. The agent will automatically see and be able to use it
371+
Example:
372+
```python
373+
# tools/my_tool.py
374+
TOOL_SCHEMA = {
375+
"type": "function",
376+
"function": {
377+
"name": "my_tool",
378+
"description": "What this tool does",
379+
"parameters": {
380+
"type": "object",
381+
"properties": {"query": {"type": "string", "description": "..."}},
382+
"required": ["query"]
383+
}
384+
}
385+
}
386+
387+
async def my_tool(query: str) -> dict:
388+
# Implementation
389+
return {"result": "..."}
390+
```
322391

323392
### tools/web_search.py
324393
Web search using OpenRouter's native web search plugin.
325394

395+
**Auto-discovery:** Exports `TOOL_SCHEMA` for automatic registration.
396+
326397
**How it works:**
327398
- Uses `plugins: [{id: "web"}]` parameter in OpenRouter API
328399
- Returns search results with source citations (URLs, titles, snippets)
@@ -337,8 +408,11 @@ async def web_search(query: str, max_results: int = 5) -> dict[str, Any]:
337408
### tools/image_generation.py
338409
Image generation using Gemini 3 Pro via OpenRouter.
339410

340-
**How it works:**
341-
- Loads reference images from `assets/` folder (up to 2 randomly selected)
411+
**Auto-discovery:** Exports `TOOL_SCHEMA` for automatic registration.
412+
413+
**How it works (v1.3):**
414+
- Loads ALL reference images from `assets/` folder (not random selection)
415+
- Supports `.png`, `.jpg`, `.jpeg`, `.jfif`, `.gif`, `.webp` formats
342416
- Sends reference images + prompt to model for consistent character appearance
343417
- Returns raw image bytes (PNG format)
344418

@@ -401,59 +475,62 @@ CREATE TABLE bot_state (
401475
→ Database.save_post()
402476
```
403477

404-
### Mention Handling
478+
### Mention Handling (Agent Architecture v1.3)
405479
```
406480
[Scheduler]
407-
MentionHandler.process_mentions_batch()
481+
MentionAgentHandler.process_mentions_batch()
408482
→ TwitterClient.get_mentions()
409483
→ Database.mention_exists() [filter processed]
410-
→ LLMClient.generate_structured()
411-
→ returns {selected_tweet_id, text, include_picture, reasoning}
412-
→ [if selected] ImageGenerator.generate() [optional]
413-
→ [if selected] TwitterClient.upload_media() [optional]
414-
→ [if selected] TwitterClient.reply()
415-
→ Database.save_mention()
484+
→ [optional] Filter by MENTIONS_WHITELIST
485+
→ LLM Call #1: Select mentions
486+
→ returns {selected_mentions: [{tweet_id, priority, reasoning, suggested_approach}]}
487+
→ For EACH selected mention:
488+
→ Database.get_user_mention_history()
489+
→ LLM Call #2: Create plan
490+
→ returns {reasoning, plan: [{tool, params}]}
491+
→ For each tool in plan:
492+
→ Execute tool (web_search or generate_image)
493+
→ Add result to conversation
494+
→ LLM Call #3: Generate reply
495+
→ returns {reply_text}
496+
→ [if image_bytes] TwitterClient.upload_media()
497+
→ TwitterClient.reply()
498+
→ Database.save_mention(tools_used=...)
416499
```
417500

418501
---
419502

420503
## Extension Points
421504

422-
### Adding a new tool
423-
1. Create `tools/my_tool.py`:
424-
```python
425-
async def my_tool(query: str) -> str:
426-
# Implementation
427-
return result
428-
```
505+
### Adding a new tool (Auto-discovery v1.3)
506+
Create `tools/my_tool.py` with `TOOL_SCHEMA` — no registry changes needed:
429507

430-
2. Update `tools/registry.py`:
431508
```python
432-
from tools.my_tool import my_tool
433-
434-
TOOLS = {
435-
"my_tool": my_tool
436-
}
437-
438-
TOOLS_SCHEMA = [
439-
{
440-
"type": "function",
441-
"function": {
442-
"name": "my_tool",
443-
"description": "What this tool does",
444-
"parameters": {
445-
"type": "object",
446-
"properties": {
447-
"query": {"type": "string", "description": "..."}
448-
},
449-
"required": ["query"]
450-
}
509+
# tools/my_tool.py
510+
511+
TOOL_SCHEMA = {
512+
"type": "function",
513+
"function": {
514+
"name": "my_tool",
515+
"description": "What this tool does",
516+
"parameters": {
517+
"type": "object",
518+
"properties": {
519+
"query": {"type": "string", "description": "Search query"}
520+
},
521+
"required": ["query"]
451522
}
452523
}
453-
]
524+
}
525+
526+
async def my_tool(query: str) -> dict:
527+
# Implementation
528+
return {"result": "..."}
454529
```
455530

456-
3. Update `services/mentions.py` to handle tool execution in the response flow.
531+
The tool is automatically discovered and available to agents on restart.
532+
533+
**Note:** To handle new tools in mentions, add execution logic to `services/mentions.py` `_process_single_mention()` method.
457534

458535
### Changing LLM model
459536
Edit `services/llm.py`, change `TEXT_MODEL` constant to any OpenRouter model ID.

README.md

Lines changed: 35 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -113,16 +113,18 @@ Each layer feeds into the next. Your agent behaves consistently across thousands
113113

114114
## Architecture
115115

116-
The system operates on **two triggers**:
116+
The system operates on **two autonomous agent triggers**:
117117

118-
| Scheduled Posts (Agent) | Mention Responses |
119-
|-------------------------|-------------------|
118+
| Scheduled Posts (Agent) | Mention Responses (Agent) |
119+
|-------------------------|---------------------------|
120120
| Cron-based (configurable interval) | Polling-based (configurable interval) |
121-
| Agent creates plan → executes tools → generates post | Handles mentions & replies |
122-
| Dynamic tool usage (web search, image generation) | LLM chooses which mention to reply to |
123-
| Posts to Twitter with optional media | LLM generates response + optional image |
121+
| Agent creates plan → executes tools → generates post | Agent selects mentions → plans per mention → generates replies |
122+
| Dynamic tool usage (web search, image generation) | 3 LLM calls per mention (select → plan → reply) |
123+
| Posts to Twitter with optional media | Tracks tools used per reply |
124124

125-
**Agent Architecture:** The autoposting system uses an autonomous agent that decides which tools to use based on context. It can search the web for current information, generate images, or post without any tools — whatever makes the best tweet.
125+
**Agent Architecture:** Both systems use autonomous agents that decide which tools to use based on context. The mention agent can process multiple mentions per batch, creating individual plans for each selected mention.
126+
127+
**Auto-Discovery Tools:** Tools are automatically discovered from the `tools/` directory. Add a new tool file with `TOOL_SCHEMA` and it's available to agents without any registry changes.
126128

127129
This separation keeps the codebase simple while enabling both proactive and reactive behavior.
128130

@@ -223,8 +225,10 @@ my-agent/
223225
│ │ ├── beliefs.py # Values and priorities
224226
│ │ └── instructions.py # Communication style
225227
│ └── prompts/ # LLM prompts (modular)
226-
│ ├── agent_autopost.py # Agent planning prompt
227-
│ └── mention_selector.py # Mention handling prompt
228+
│ ├── agent_autopost.py # Agent planning prompt
229+
│ ├── mention_selector.py # Legacy mention selector (v1.2)
230+
│ ├── mention_selector_agent.py # Agent mention selection (v1.3)
231+
│ └── mention_reply_agent.py # Agent reply planning (v1.3)
228232
229233
├── utils/
230234
│ └── api.py # OpenRouter API configuration
@@ -238,9 +242,9 @@ my-agent/
238242
│ └── database.py # PostgreSQL for history + metrics
239243
240244
├── tools/
241-
│ ├── registry.py # Available tools for LLM
245+
│ ├── registry.py # Auto-discovery tool registry
242246
│ ├── web_search.py # Web search via OpenRouter plugins
243-
│ └── image_generation.py # Image generation tool
247+
│ └── image_generation.py # Image generation with reference images
244248
245249
├── main.py # FastAPI + APScheduler entry point
246250
├── requirements.txt # Dependencies
@@ -294,35 +298,42 @@ Agent thinks: "I want to post about crypto trends with a visual"
294298

295299
### Mention Handler (`services/mentions.py`)
296300

297-
Monitors Twitter mentions and generates contextual replies using polling.
301+
Agent-based mention processing with 3 LLM calls per mention (v1.3).
298302

299303
**How it works:**
300304
1. Polls Twitter API for new mentions every 20 minutes (configurable)
301305
2. Filters out already-processed mentions using database
302-
3. Sends all new mentions to LLM in a single request
303-
4. LLM selects which mention to reply to (or none if not worth replying)
304-
5. LLM returns structured response: `{selected_tweet_id, text, include_picture, reasoning}`
305-
6. If a mention is selected, generates optional image and posts reply
306-
7. Saves mention interaction to database for history
307-
308-
**Why single-call selection:** Instead of replying to every mention, the LLM evaluates all pending mentions and picks the most interesting one. This creates more authentic engagement — your agent chooses conversations worth having, just like a real person would.
306+
3. **LLM #1: Selection** — Evaluates all mentions, returns array of worth replying to (with priority)
307+
4. For EACH selected mention:
308+
- Gets user conversation history from database
309+
- **LLM #2: Planning** — Creates plan (which tools to use)
310+
- Executes tools (web_search, generate_image)
311+
- **LLM #3: Reply** — Generates final reply text
312+
- Uploads image if generated, posts reply
313+
- Saves interaction with tools_used tracking
314+
5. Returns batch summary
315+
316+
**Why agent architecture:** Instead of a single LLM call for all mentions, each mention gets individual attention. The agent can use tools to research topics, generate custom images, and craft contextually appropriate replies. User conversation history enables personalized interactions.
309317

310318
**Configuration:**
311319
- `MENTIONS_INTERVAL_MINUTES` — Time between mention checks (default: 20)
320+
- `MENTIONS_WHITELIST` — Optional list of usernames for testing (empty = all users)
312321
- Requires Twitter API Basic tier or higher for mention access
313322

314-
### Image Generation (`services/image_gen.py`)
323+
### Image Generation (`tools/image_generation.py`)
315324

316325
Generates images using Gemini 3 Pro via OpenRouter, with support for reference images.
317326

318-
**How `assets/` folder works:**
319-
- Place 1 or more reference images in `assets/` folder (supports: jpg, png, jpeg, gif, webp)
320-
- Bot randomly selects up to 2 images as style/character reference
327+
**How `assets/` folder works (v1.3):**
328+
- Place reference images in `assets/` folder (supports: png, jpg, jpeg, gif, webp, jfif)
329+
- Bot uses **ALL** reference images (not random selection) for maximum consistency
321330
- Reference images are sent to the model along with the generation prompt
322331
- If `assets/` is empty, images are generated without reference (pure text-to-image)
323332
- Use reference images to maintain consistent character appearance across posts
324333

325-
**Example use case:** Place photos of your bot's character/avatar in `assets/`. The model will use these as reference when generating new images, keeping the visual style consistent.
334+
**Auto-discovery:** Tool exports `TOOL_SCHEMA` and is automatically available to agents.
335+
336+
**Example use case:** Place photos of your bot's character/avatar in `assets/`. The model will use all of them as reference when generating new images, keeping the visual style consistent.
326337

327338
### Personality (`config/personality/`)
328339

0 commit comments

Comments
 (0)