An AI-powered personal chat agent for WhatsApp and iMessage with configurable personality and policy-driven responses
Project description
Wingman
An AI-powered personal chat agent that participates in conversations with configurable personality, tone adaptation based on contact relationships, and policy-driven response rules. Supports multiple platforms including WhatsApp and iMessage (with more coming). Built with OpenAI GPT models.
Features
- Personality-driven responses - Configurable bot personality with witty, casual tone
- Role-based tone adaptation - Different tones for different relationships (partner, family, friends)
- Policy-driven rules - Fine-grained control over when to respond
- Safety controls - Rate limiting, cooldowns, quiet hours
- Conversation memory - Context-aware responses using message history
- Hot-reload config - Changes to contacts/groups apply without restart
- Background daemon - Runs as a macOS service with auto-restart
Architecture
┌─────────────────────────────────────────────────────────────┐
│ WhatsApp Web │
└─────────────────────────┬───────────────────────────────────┘
│
┌─────────────────────────▼───────────────────────────────────┐
│ Node.js Listener (Baileys) │
│ node_listener/ │
│ - Connects to WhatsApp Web │
│ - Receives messages via WebSocket │
│ - Sends messages back │
└─────────────────────────┬───────────────────────────────────┘
│ IPC (stdin/stdout)
┌─────────────────────────▼───────────────────────────────────┐
│ Python Orchestrator │
│ python_orchestrator/ │
│ - Message processing & filtering │
│ - Config-driven identity (contacts.yaml, groups.yaml) │
│ - Policy-based response rules (policies.yaml) │
│ - Role-based tone adaptation │
│ - OpenAI GPT integration │
│ - Conversation memory (SQLite) │
│ - Safety controls (rate limits, quiet hours, cooldowns) │
└─────────────────────────────────────────────────────────────┘
Prerequisites
- macOS (tested on macOS, may work on Linux with modifications)
- Python 3.10+
- Node.js 18+
- WhatsApp account (on your phone)
- OpenAI API key
Installation
Quick Install (Recommended)
pip install wingman-ai
Then run the interactive setup:
wingman init
The setup wizard will:
- Check prerequisites (Python, Node.js, npm)
- Configure your OpenAI API key
- Set up bot personality and name
- Configure safety settings
- Install the WhatsApp listener
Development Install
git clone https://github.com/metanoia-oss/wingman.git
cd wingman
pip install -e .
wingman init
CLI Commands
| Command | Description |
|---|---|
wingman init |
Interactive setup wizard |
wingman auth |
Connect WhatsApp (scan QR code) |
wingman start |
Start bot as background daemon |
wingman start -f |
Start bot in foreground |
wingman stop |
Stop running bot |
wingman status |
Check if running |
wingman logs |
View/stream activity logs |
wingman config |
View or edit configuration |
wingman uninstall |
Remove Wingman and all data |
Quick Start
# 1. Install
pip install wingman-ai
# 2. Run setup wizard
wingman init
# 3. Connect WhatsApp
wingman auth
# 4. Start the bot
wingman start
# 5. Check status
wingman status
# 6. View logs
wingman logs
Running the Bot
First Run: WhatsApp Authentication
After setup, connect WhatsApp by scanning a QR code:
wingman auth
- A QR code will appear in the terminal
- Open WhatsApp on your phone
- Go to Settings > Linked Devices > Link a Device
- Scan the QR code
- Wait for "Connected!" message
Start as Background Service
wingman start
This starts Wingman as a macOS launchd service that auto-restarts on crash.
Run in Foreground
wingman start --foreground
Press Ctrl+C to stop.
View Logs
# Stream logs in real-time
wingman logs
# Show last 100 lines without streaming
wingman logs --no-follow -n 100
# Show error logs
wingman logs --error
Legacy Installation (Manual)
Click to expand manual installation steps
Step 1: Clone the Repository
git clone https://github.com/metanoia-oss/wingman.git
cd wingman
Step 2: Set Up Python Environment
python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
Step 3: Build Node.js Listener
cd node_listener
npm install
npm run build
cd ..
Step 4: Configure Environment
cp .env.example .env
nano .env # Add your OPENAI_API_KEY
Step 5: Configure Contacts & Policies
cp config/contacts.yaml.example config/contacts.yaml
cp config/groups.yaml.example config/groups.yaml
cp config/policies.yaml.example config/policies.yaml
Run with Legacy Script
python run.py
Configuration
Config File (~/.config/wingman/config.yaml)
bot:
name: "Wingman"
openai:
api_key: "sk-..." # Or use OPENAI_API_KEY env var
model: "gpt-4o"
max_response_tokens: 150
temperature: 0.8
personality:
base_prompt: "You are Wingman, a witty and helpful assistant."
default_tone: casual
safety:
max_replies_per_hour: 30
cooldown_seconds: 60
quiet_hours:
enabled: true
start: 0
end: 6
Environment Variables (Legacy)
These are still supported for backward compatibility:
| Variable | Default | Description |
|---|---|---|
OPENAI_API_KEY |
(required) | Your OpenAI API key |
OPENAI_MODEL |
gpt-4o |
Model to use (gpt-4o, gpt-4-turbo, gpt-3.5-turbo) |
BOT_NAME |
Maximus |
Name the bot responds to |
MAX_REPLIES_PER_HOUR |
30 |
Rate limit for replies |
DEFAULT_COOLDOWN_SECONDS |
60 |
Minimum seconds between replies |
QUIET_HOURS_START |
0 |
Hour to start quiet mode (0-23) |
QUIET_HOURS_END |
6 |
Hour to end quiet mode (0-23) |
CONTEXT_WINDOW_SIZE |
30 |
Number of messages to include as context |
MAX_RESPONSE_TOKENS |
150 |
Maximum response length |
TEMPERATURE |
0.8 |
Creativity (0.0-1.0) |
Contact Configuration (config/contacts.yaml)
Maps WhatsApp JIDs to contact profiles with roles and tone preferences.
contacts:
"+14155551234@s.whatsapp.net":
name: "Partner"
role: girlfriend # girlfriend, sister, friend, family, colleague, unknown
tone: affectionate # loving, affectionate, friendly, casual, sarcastic, neutral
allow_proactive: true
cooldown_override: 30 # Custom cooldown (seconds)
defaults:
role: unknown
tone: neutral
allow_proactive: false
Available Tones:
| Tone | Behavior |
|---|---|
loving |
Deep affection and intimacy (for partners) |
affectionate |
Warm, caring, supportive |
friendly |
Playful teasing, sibling vibes |
casual |
Relaxed friend energy |
sarcastic |
Witty banter, dry humor |
neutral |
Polite, maintains boundaries |
Group Configuration (config/groups.yaml)
Maps group JIDs to categories and reply policies.
groups:
"120363012345678901@g.us":
name: "Family Chat"
category: family # family, friends, work, unknown
reply_policy: always # always, selective, never
defaults:
category: unknown
reply_policy: selective
Reply Policies:
| Policy | Behavior |
|---|---|
always |
Always respond to messages |
selective |
Only respond when @mentioned or replying to bot |
never |
Never respond in this chat |
Policy Rules (config/policies.yaml)
Rules for determining when to respond. Evaluated in order; first match wins.
rules:
- name: "girlfriend_dm"
conditions:
is_dm: true
role: girlfriend
action: always
- name: "unknown_dm"
conditions:
is_dm: true
role: unknown
action: never
- name: "work_group"
conditions:
is_group: true
group_category: work
action: never
fallback:
action: selective
Available Conditions:
is_dm: true/false- Is this a direct message?is_group: true/false- Is this a group chat?role: girlfriend/sister/friend/...- Contact's rolegroup_category: family/friends/work/...- Group's categoryis_reply_to_bot: true/false- Is this a reply to bot's message?
Finding JIDs
To find contact and group JIDs:
- Send a message to the contact/group
- Check the logs:
./scripts/daemon.sh logs - Look for log entries like:
Processing message: chat=+14155551234@s.whatsapp.net... Resolved: contact=Unknown (role=unknown, tone=neutral)
The chat= value is the JID you need.
Project Structure
wingman/
├── pyproject.toml # Package configuration
├── README.md # This file
├── LICENSE # MIT License
│
├── src/wingman/ # Main Python package
│ ├── cli/ # CLI commands (typer)
│ │ ├── main.py # CLI entry point
│ │ ├── wizard.py # Setup wizard
│ │ └── commands/ # Individual commands
│ ├── config/ # Configuration
│ │ ├── paths.py # XDG path management
│ │ ├── settings.py # Settings loader
│ │ ├── registry.py # Contact/Group registries
│ │ └── personality.py # Bot personality prompts
│ ├── core/ # Core bot logic
│ │ ├── agent.py # Main agent class
│ │ ├── process_manager.py # Node.js subprocess
│ │ ├── message_processor.py # Message handling
│ │ ├── llm/ # OpenAI integration
│ │ ├── memory/ # SQLite conversation storage
│ │ ├── safety/ # Rate limits, quiet hours
│ │ ├── policy/ # Policy evaluation
│ │ └── transports/ # WhatsApp & iMessage
│ ├── daemon/ # Background service management
│ └── installer/ # Node.js listener installer
│
├── node_listener/ # WhatsApp Web connection
│ ├── src/ # TypeScript source
│ ├── package.json
│ └── tsconfig.json
│
└── config/ # Example configs (legacy)
├── contacts.yaml.example
├── groups.yaml.example
└── policies.yaml.example
User Config Locations (XDG)
After running wingman init, config files are stored in:
~/.config/wingman/
├── config.yaml # Main configuration
├── contacts.yaml # Contact profiles
├── groups.yaml # Group settings
├── policies.yaml # Response policies
└── node_listener/ # Installed Node.js listener
~/.local/share/wingman/
├── conversations.db # SQLite database
└── auth_state/ # WhatsApp credentials
~/.cache/wingman/
└── logs/ # Log files
Troubleshooting
QR Code Won't Scan
- Make sure terminal is large enough to display the QR code
- Try reducing terminal font size
- Ensure good lighting when scanning
Bot Not Responding
-
Check if daemon is running:
wingman status -
Check logs for errors:
wingman logs --error
-
Verify OpenAI API key is valid:
wingman config --show
-
Check policy rules - the contact may be set to
neverrespond
"WhatsApp Logged Out" Error
WhatsApp may log out linked devices periodically. Re-authenticate:
wingman stop
wingman auth
wingman start
Config Changes Not Working
Config files auto-reload every 2 seconds. If changes aren't applying:
- Check for YAML syntax errors:
wingman config --show
- Check the logs for reload messages:
wingman logs
Important Notes
- Mac must stay logged in - The daemon runs as a user agent. It won't run when logged out.
- Screen lock is OK - The bot continues running when the screen is locked.
- Auto-restart - If the bot crashes, launchd will automatically restart it.
- Rate limiting - Built-in rate limits prevent spam. Configure in
.env.
Uninstalling
To completely remove Wingman:
# Remove all Wingman data, config, and daemon
wingman uninstall
# Then remove the package
pip uninstall wingman-ai
Options:
wingman uninstall --keep-config- Keep config files, only remove datawingman uninstall --force- Don't ask for confirmation
Contributing
Contributions are welcome! Please see CONTRIBUTING.md for guidelines.
License
This project is licensed under the MIT License - see the LICENSE file for details.
Acknowledgments
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file wingman_ai-1.1.2.tar.gz.
File metadata
- Download URL: wingman_ai-1.1.2.tar.gz
- Upload date:
- Size: 88.3 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
4bf4caee6db5668e3d7f54b9e29b4c1f2d00af2471fb6fe9d2e774a7aaf161ba
|
|
| MD5 |
2ee6f3be80710e64a28df994ef16e7ed
|
|
| BLAKE2b-256 |
b0dc513536f94d5845e298b52c389cf305dccea5511a7cd21132b5e7dc36800f
|
Provenance
The following attestation bundles were made for wingman_ai-1.1.2.tar.gz:
Publisher:
release-pypi.yml on metanoia-oss/wingman
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
wingman_ai-1.1.2.tar.gz -
Subject digest:
4bf4caee6db5668e3d7f54b9e29b4c1f2d00af2471fb6fe9d2e774a7aaf161ba - Sigstore transparency entry: 922747159
- Sigstore integration time:
-
Permalink:
metanoia-oss/wingman@3e53215d4199ce3b1bef1ac6c2ae368946fee018 -
Branch / Tag:
refs/tags/v1.1.2 - Owner: https://github.com/metanoia-oss
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release-pypi.yml@3e53215d4199ce3b1bef1ac6c2ae368946fee018 -
Trigger Event:
release
-
Statement type:
File details
Details for the file wingman_ai-1.1.2-py3-none-any.whl.
File metadata
- Download URL: wingman_ai-1.1.2-py3-none-any.whl
- Upload date:
- Size: 120.8 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
6637a240ccf2ecf9a559fd26ad45caedc8d4c4bd9879430390f96a733e4589bd
|
|
| MD5 |
09e30cb09bc0bccda6b5375f7de99367
|
|
| BLAKE2b-256 |
875d5b9f577addf19f35e410ecc08b043df11548105783e8ef0b1e138ee9b823
|
Provenance
The following attestation bundles were made for wingman_ai-1.1.2-py3-none-any.whl:
Publisher:
release-pypi.yml on metanoia-oss/wingman
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
wingman_ai-1.1.2-py3-none-any.whl -
Subject digest:
6637a240ccf2ecf9a559fd26ad45caedc8d4c4bd9879430390f96a733e4589bd - Sigstore transparency entry: 922747221
- Sigstore integration time:
-
Permalink:
metanoia-oss/wingman@3e53215d4199ce3b1bef1ac6c2ae368946fee018 -
Branch / Tag:
refs/tags/v1.1.2 - Owner: https://github.com/metanoia-oss
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release-pypi.yml@3e53215d4199ce3b1bef1ac6c2ae368946fee018 -
Trigger Event:
release
-
Statement type: