diff --git a/AGENTS.md b/AGENTS.md index 9bc4b90..1f04103 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -15,6 +15,7 @@ Modules can have TECHNICAL.md files that document their technical implementation - `modules/imap/TECHNICAL.md` - Technical documentation for the IMAP module - `modules/openai/TECHNICAL.md` - Technical documentation for the OpenAI module +- `modules/slack/TECHNICAL.md` - Technical documentation for the Slack integration module ## Coding diff --git a/modules/openai/class-openai-module.php b/modules/openai/class-openai-module.php index 032a010..ece6569 100644 --- a/modules/openai/class-openai-module.php +++ b/modules/openai/class-openai-module.php @@ -48,6 +48,8 @@ public function register() { $this->register_block( 'message', array() ); require_once __DIR__ . '/chat-page.php'; + require_once __DIR__ . '/voice-chat-page.php'; + require_once __DIR__ . '/custom-gpt-page.php'; $this->email_responder = new OpenAI_Email_Responder( $this ); @@ -81,6 +83,7 @@ public function register_abilities() { 'input_schema' => array( 'type' => 'object', 'properties' => array( + // phpcs:ignore WordPress.WP.PostsPerPage -- This is a schema definition, not a query parameter. 'posts_per_page' => array( 'type' => 'integer', 'description' => 'Number of posts to return. Default to 10', @@ -416,7 +419,11 @@ public function cli_openai_responses( array $args, array $assoc_args = array() ) ) ); } - $messages_payload = file_get_contents( $file_path ); + // Reading local file path provided via CLI. + global $wp_filesystem; + require_once ABSPATH . '/wp-admin/includes/file.php'; + WP_Filesystem(); + $messages_payload = $wp_filesystem->get_contents( $file_path ); if ( false === $messages_payload ) { WP_CLI::error( sprintf( @@ -588,7 +595,7 @@ public function admin_menu() { 'Custom GPT', 'manage_options', 'pos-custom-gpt', - array( $this, 'custom_gpt_page' ) + 'pos_render_custom_gpt_page' ); add_submenu_page( 'personalos', @@ -596,7 +603,7 @@ public function admin_menu() { 'Voice Chat', 'manage_options', 'pos-voice-chat', - array( $this, 'voice_chat_page' ) + 'pos_render_voice_chat_page' ); add_action( 'admin_head', @@ -705,257 +712,7 @@ public function get_system_state_ability( $args ) { ); } - public function voice_chat_page() { - echo << -
- -
- OpenAI advanced voice mode -
-
-
- -
-
- - -
-
- 🎤 - - 🎧 - -
- - - EOF; - - wp_enqueue_script( 'voice-chat', plugins_url( 'assets/voice-chat.js', __FILE__ ), array( 'wp-api-fetch' ), time(), true ); - //wp_enqueue_style( 'voice-chat', plugins_url( 'assets/voice-chat.css', __FILE__ ) ); - - } - - public function custom_gpt_page() { - echo <<Configure your custom GPT -

First create a new custom GPT

-

System prompt

-

This is the system prompt for your custom GPT. Modify it to fit your needs.

- - EOF; - $schema = file_get_contents( plugin_dir_path( __FILE__ ) . 'chatgpt_routes.json' ); - $schema = json_decode( $schema, true ); - $schema['servers'][0]['url'] = get_rest_url( null, '' ); - $schema = wp_json_encode( $schema, JSON_PRETTY_PRINT ); - $schema = wp_unslash( $schema ); - $login = esc_attr( wp_get_current_user()->user_login ); - $schema = esc_textarea( $schema ); - echo <<Schema -

This is the schema for your custom GPT. It describes the API endpoints that your GPT can use. Copy it into your ChatGPT configuration.

- - EOF; - - echo <<Auth -

You can use basic request and application passwords to authenticate your requests:

-
    -
  1. Create an Application Password for your user
  2. -
  3. Paste the password and encode below using base64
  4. -
  5. Use the encoded password as the token for basic auth in your ChatGPT configuration
  6. -
-

Encode password

- - - -

-		
-		EOF;
-	}
 
 	public function rest_api_init() {
 		register_rest_route(
@@ -1653,8 +1410,8 @@ private function execute_ability( $tool_name, $arguments ) {
 		return $ability->execute( $arguments );
 	}
 
-	public function complete_backscroll( array $backscroll, callable $callback = null ) {
-		$prompt_config = $this->get_prompt_config();
+	public function complete_backscroll( array $backscroll, callable $callback = null, ?\WP_Post $prompt = null ) {
+		$prompt_config = $this->get_prompt_config( $prompt );
 		$tool_definitions = $this->get_abilities_as_tools();
 		$max_loops = 10;
 		do {
@@ -2430,6 +2187,7 @@ public function tts( $messages, $voice = 'ballad', $data = array() ) {
 
 		$this->log( 'data from response: ' . print_r( $response->choices[0]->message->content, true ) );
 		// Decode base64 audio data and write to temp file
+		// phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode -- Decoding audio data from OpenAI API response (benign use).
 		$audio_data = base64_decode( $response->choices[0]->message->audio->data );
 		$wp_filesystem->put_contents( $tempfile, $audio_data );
 
diff --git a/modules/openai/custom-gpt-page.php b/modules/openai/custom-gpt-page.php
new file mode 100644
index 0000000..23d215c
--- /dev/null
+++ b/modules/openai/custom-gpt-page.php
@@ -0,0 +1,93 @@
+
+	

Configure your custom GPT

+

First create a new custom GPT

+

System prompt

+

This is the system prompt for your custom GPT. Modify it to fit your needs.

+ + get_contents( $schema_file ); + $schema = json_decode( $schema, true ); + $schema['servers'][0]['url'] = get_rest_url( null, '' ); + $schema = wp_json_encode( $schema, JSON_PRETTY_PRINT ); + $schema = wp_unslash( $schema ); + $login = esc_attr( wp_get_current_user()->user_login ); + $schema = esc_textarea( $schema ); + ob_start(); + ?> +

Schema

+

This is the schema for your custom GPT. It describes the API endpoints that your GPT can use. Copy it into your ChatGPT configuration.

+ + +

Auth

+

You can use basic request and application passwords to authenticate your requests:

+
    +
  1. Create an Application Password for your user
  2. +
  3. Paste the password and encode below using base64
  4. +
  5. Use the encoded password as the token for basic auth in your ChatGPT configuration
  6. +
+

Encode password

+ + + +

+	
+	
+		
+ +
+ OpenAI advanced voice mode +
+
+
+ +
+
+ + +
+
+ 🎤 + + 🎧 + +
+ + + EOF; + + $openai_module_file = __DIR__ . '/class-openai-module.php'; + wp_enqueue_script( 'voice-chat', plugins_url( 'assets/voice-chat.js', $openai_module_file ), array( 'wp-api-fetch' ), time(), true ); + //wp_enqueue_style( 'voice-chat', plugins_url( 'assets/voice-chat.css', $openai_module_file ) ); +} + diff --git a/modules/slack/README.md b/modules/slack/README.md new file mode 100644 index 0000000..5cef3ed --- /dev/null +++ b/modules/slack/README.md @@ -0,0 +1,179 @@ +# Slack Module - User Guide + +Welcome to the Slack Module for PersonalOS! This module enables AI-powered conversations directly in your Slack workspace. + +## What This Module Does + +The Slack module connects PersonalOS to your Slack workspace and: +- **Responds to mentions**: When you @mention your bot, it responds with AI-generated messages +- **Handles direct messages**: DMs to your bot trigger AI responses +- **Maintains conversation context**: Threaded conversations preserve full context +- **Supports channel-specific prompts**: Different Slack channels can use different AI personalities/prompts + +## Requirements + +1. A Slack workspace where you have permission to install apps +2. The OpenAI module must be configured and active +3. The Notes module must be active (for channel-specific prompts) + +## Setup Instructions + +### Step 1: Create a Slack App + +1. Go to [api.slack.com/apps](https://api.slack.com/apps) +2. Click **Create New App** → **From scratch** +3. Enter a name (e.g., "PersonalOS Bot") and select your workspace +4. Click **Create App** + +### Step 2: Configure Bot Permissions + +1. In your app settings, go to **OAuth & Permissions** +2. Under **Scopes** → **Bot Token Scopes**, add these permissions: + - `app_mentions:read` - Read messages that mention the bot + - `channels:history` - Read messages in public channels + - `chat:write` - Send messages as the bot + - `groups:history` - Read messages in private channels + - `im:history` - Read direct messages + +3. Click **Install to Workspace** and authorize the app +4. Copy the **Bot User OAuth Token** (starts with `xoxb-`) + +### Step 3: Set Up Event Subscriptions + +1. Go to **Event Subscriptions** in your app settings +2. Toggle **Enable Events** to ON +3. For the **Request URL**, enter: + ``` + https://your-wordpress-site.com/wp-json/pos/v1/slack/callback + ``` +4. Wait for Slack to verify the URL (PersonalOS handles this automatically) + +5. Under **Subscribe to bot events**, add: + - `app_mention` - When someone mentions your bot + - `message.im` - When someone sends a direct message to your bot + +6. Click **Save Changes** + +### Step 4: Get Your Verification Token + +1. Go to **Basic Information** in your app settings +2. Under **App Credentials**, find the **Verification Token** +3. Copy this token + +### Step 5: Configure PersonalOS + +1. Go to **PersonalOS Settings** in your WordPress admin +2. Find the **Slack integration** section +3. Enter: + - **Slack Verification Token**: The verification token from Step 4 + - **Slack API Token**: The Bot User OAuth Token from Step 2 +4. Click **Save Changes** + +### Step 6: Test It + +1. Invite your bot to a channel: `/invite @YourBotName` +2. Send a message mentioning your bot: `@YourBotName Hello!` +3. The bot should respond with an AI-generated message + +## Using Channel-Specific Prompts + +One of the most powerful features is the ability to assign different AI prompts to different Slack channels. This lets you create specialized bots for different purposes. + +### How It Works + +1. Create a prompt in PersonalOS (under Notes → Prompts notebook) +2. Add a custom field `slack_channel_id` with the Slack channel ID +3. When messages come from that channel, the bot uses your custom prompt + +### Step-by-Step Setup + +#### 1. Find Your Slack Channel ID + +- Open Slack in a browser +- Navigate to the channel you want to customize +- The channel ID is in the URL: `https://app.slack.com/client/TXXXXXX/CXXXXXXXXX` +- The `CXXXXXXXXX` part is your channel ID + +**Or via Slack:** +- Right-click on the channel name +- Select "View channel details" +- Scroll to the bottom to find the Channel ID + +#### 2. Create a Custom Prompt + +1. In WordPress, go to **Notes** and create a new note +2. Add it to the **Prompts** notebook (taxonomy) +3. Write your custom system prompt, for example: + ``` + You are a helpful coding assistant. Focus on providing clear, + well-documented code examples. Always explain your reasoning. + ``` + +#### 3. Link the Prompt to a Channel + +1. In the WordPress editor, add a custom field to your prompt: + - Field name: `slack_channel_id` + - Field value: Your channel ID (e.g., `C0123456789`) + +2. Save the prompt + +#### 4. Test It + +Send a message in that Slack channel mentioning your bot. It should now respond using your custom prompt! + +### Example Use Cases + +| Channel | Prompt Purpose | +|---------|----------------| +| #coding-help | Technical assistant focused on code review and debugging | +| #writing | Creative writing assistant with editorial voice | +| #general | Friendly, casual conversational assistant | +| #customer-support | Professional support agent tone | + +## Features + +### Threaded Conversations + +When you reply in a Slack thread, the bot retrieves the full conversation history and uses it as context. This means: +- Follow-up questions work naturally +- The bot remembers what was discussed earlier in the thread +- Long conversations maintain coherence + +### Markdown Conversion + +The bot automatically converts AI responses from markdown to Slack's formatting: +- `**bold**` becomes `*bold*` +- `[links](url)` become `` +- Code blocks are preserved + +### Bot Message Filtering + +The bot ignores its own messages and messages from other bots to prevent infinite loops. + +## Troubleshooting + +**"Bot doesn't respond to messages"** +- Verify the bot is invited to the channel +- Check that Event Subscriptions URL is verified +- Ensure both tokens are correctly entered in PersonalOS settings +- Check WordPress debug log for errors + +**"Getting 403 errors"** +- Verify the Verification Token matches exactly +- Make sure your WordPress site is accessible from the internet (not localhost) + +**"Bot responds but with generic answers"** +- Check if your channel-specific prompt is properly linked +- Verify the `slack_channel_id` custom field is set correctly +- The channel ID must match exactly (case-sensitive) + +**"Thread context isn't working"** +- Ensure the bot has `channels:history` and `groups:history` permissions +- The bot must be a member of the channel to read history + +--- + +## Technical Details + +For developers who want to extend or customize the Slack module, see [TECHNICAL.md](TECHNICAL.md) for hook documentation and advanced configuration options. + diff --git a/modules/slack/TECHNICAL.md b/modules/slack/TECHNICAL.md new file mode 100644 index 0000000..621deda --- /dev/null +++ b/modules/slack/TECHNICAL.md @@ -0,0 +1,281 @@ +# Slack Module - Technical Documentation + +This document provides technical details for developers who want to extend or customize the Slack integration module. + +## Architecture Overview + +The Slack module follows a webhook-based architecture: + +1. **Incoming Webhook**: Slack sends events to `/wp-json/pos/v1/slack/callback` +2. **Immediate Response**: The handler responds quickly to Slack (avoiding timeout) +3. **Background Processing**: Actual AI processing happens via WordPress cron +4. **Outgoing API Call**: Bot responds in the thread via Slack's `chat.postMessage` API + +## REST API Endpoints + +### POST `/wp-json/pos/v1/slack/callback` + +Main webhook endpoint for Slack events. + +**Authentication**: Validates `token` field against stored verification token. + +**Supported Event Types**: +- `url_verification` - Slack URL verification challenge +- `event_callback` - Standard event wrapper containing: + - `app_mention` - Bot was mentioned + - `message.im` - Direct message to bot + +**Response**: Immediate `200 OK` with `{ "text": "Processing your request..." }` + +**Example Payload**: +```json +{ + "token": "verification-token", + "type": "event_callback", + "event": { + "type": "app_mention", + "user": "U1234567890", + "text": "<@U0BOT1234> Hello!", + "ts": "1234567890.123456", + "channel": "C1234567890", + "thread_ts": "1234567890.123456" + } +} +``` + +## Action Hooks + +### `pos_process_slack_callback` + +Fires during background processing of a Slack event. This is scheduled via `wp_schedule_single_event()`. + +**Parameters**: +- `$payload` (array): Full Slack event payload + +**Example Usage**: +```php +add_action( 'pos_process_slack_callback', function( $payload ) { + // Custom processing logic + $channel = $payload['event']['channel']; + $text = $payload['event']['text']; + + // Your custom handling here +}, 5 ); // Priority 5 runs before default handler (10) +``` + +## Channel-Specific Prompts + +### How It Works + +The module queries the Notes module for prompts that have a `slack_channel_id` meta field matching the incoming channel. + +```php +$matching_prompts = $notes->list( + array( + 'meta_query' => array( + array( + 'key' => 'slack_channel_id', + 'value' => $payload['event']['channel'] + ) + ) + ), + 'prompts' +); +``` + +### Meta Field Setup + +To link a prompt to a Slack channel: + +1. Create a note in the `prompts` taxonomy +2. Add post meta: + ```php + update_post_meta( $prompt_id, 'slack_channel_id', 'C0123456789' ); + ``` + +### Multiple Prompts per Channel + +If multiple prompts match a channel, one is randomly selected: + +```php +$prompt = $matching_prompts[ array_rand( $matching_prompts ) ]; +``` + +This allows for varied responses when multiple prompts are configured. + +## Public Methods + +### `slack_gpt_retrieve_backscroll( $thread, $channel )` + +Retrieves conversation history from a Slack thread. + +**Parameters**: +- `$thread` (string): Thread timestamp (`ts` or `thread_ts`) +- `$channel` (string): Channel ID + +**Returns**: array - Array of messages formatted for GPT: +```php +array( + array( + 'role' => 'user', // or 'assistant' for bot messages + 'content' => 'Message text without @mentions' + ), + // ... more messages +) +``` + +### `slack_gpt_respond_in_thread( $ts, $channel, $response )` + +Posts a response to a Slack thread with automatic markdown conversion. + +**Parameters**: +- `$ts` (string): Thread timestamp to reply to +- `$channel` (string): Channel ID +- `$response` (string): Message content (markdown supported) + +**Markdown Conversion**: +- `[text](url)` → `` (Slack link format) +- `**bold**` or `__bold__` → `*bold*` +- `` `code` `` → `` `code` `` (preserved) +- `*italic*` or `_italic_` → `_italic_` + +### `slack_message_to_gpt_message( $message )` + +Converts a Slack message object to GPT message format. + +**Parameters**: +- `$message` (object): Slack message object + +**Returns**: array +```php +array( + 'role' => 'user', // 'assistant' if message has bot_id + 'content' => 'text' // @mentions stripped +) +``` + +## Integration with OpenAI Module + +The Slack module calls the OpenAI module's `complete_backscroll()` method: + +```php +$openai = POS::get_module_by_id( 'openai' ); +$response = $openai->complete_backscroll( $backscroll, null, $prompt ); +``` + +**Parameters**: +- `$backscroll` (array): Conversation history with `old => true` flag +- `$callback` (callable|null): Not used for Slack +- `$prompt` (WP_Post|null): Optional custom prompt post + +The `old => true` flag marks messages as historical context, not requiring a response. + +## Configuration + +Settings are stored in WordPress options with the prefix `pos_slack_`. + +### Available Settings + +- `pos_slack_slack_token` - Slack verification token for webhook validation +- `pos_slack_api_token` - Bot User OAuth Token (`xoxb-...`) for API calls + +## Bot Filtering + +The module skips processing for bot messages to prevent loops: + +```php +if ( ! empty( $payload['event']['bot_id'] ) ) { + return; +} +``` + +## Background Processing + +To avoid Slack's 3-second timeout, processing is deferred: + +1. Webhook immediately returns `200 OK` +2. Event is scheduled via `wp_schedule_single_event()` +3. Cron is triggered immediately via `wp_remote_post()` to `/wp-cron.php` + +```php +wp_schedule_single_event( time(), 'pos_process_slack_callback', array( $payload ) ); +wp_remote_post( + site_url( '/wp-cron.php' ), + array( + 'timeout' => 0.01, + 'blocking' => false, + 'sslverify' => false, + ) +); +``` + +## Debugging + +Enable WordPress debug logging to see Slack module activity: + +```php +// In wp-config.php +define( 'WP_DEBUG', true ); +define( 'WP_DEBUG_LOG', true ); +``` + +The module logs: +- Callback payloads: `pos_process_slack_callback:{json}` +- Token validation failures + +Check `wp-content/debug.log` for details. + +## Extending the Module + +### Custom Response Handler + +To customize how responses are generated: + +```php +// Remove default handler +remove_action( 'pos_process_slack_callback', array( $slack_module, 'pos_process_slack_callback' ) ); + +// Add custom handler +add_action( 'pos_process_slack_callback', 'my_custom_slack_handler' ); + +function my_custom_slack_handler( $payload ) { + $slack = POS::get_module_by_id( 'slack' ); + + // Your custom logic here + $response = "Custom response"; + + $slack->slack_gpt_respond_in_thread( + $payload['event']['thread_ts'] ?? $payload['event']['ts'], + $payload['event']['channel'], + $response + ); +} +``` + +### Adding Prompt Selection Logic + +To implement custom prompt selection (e.g., based on user or message content): + +```php +add_action( 'pos_process_slack_callback', 'my_prompt_selector', 5 ); + +function my_prompt_selector( $payload ) { + // Store selected prompt in a global or option + // The default handler will use it +} +``` + +## Security Considerations + +1. **Token Validation**: All incoming webhooks validate the verification token +2. **User Context**: Processing switches to the appropriate WordPress user via `$notes->switch_to_user()` +3. **Bot Filtering**: Bot messages are ignored to prevent loops + +## Future Improvements (TODOs) + +The following enhancements are planned: + +1. **Frontend UX**: Admin interface for managing channel-prompt associations +2. **Starter Content**: Default prompts for common Slack use cases +3. **OAuth Integration**: Replace hardcoded user switching with proper Slack-to-WordPress user mapping + diff --git a/modules/slack/class-slack-module.php b/modules/slack/class-slack-module.php index 3200737..42d4d1f 100644 --- a/modules/slack/class-slack-module.php +++ b/modules/slack/class-slack-module.php @@ -109,21 +109,6 @@ public function slack_message_to_gpt_message( $message ) { ); } - /** - * Abstraction method to process OpenAI chat completions. It calls the OpenAI module's chat_assistant functionality. - * - * @param array $messages The messages array for completion. - * @return array - */ - public function process_chat_completion( array $messages ): array { - $openai = POS::get_module_by_id( 'openai' ); - if ( ! $openai || ! method_exists( $openai, 'chat_assistant' ) ) { - // Log error if OpenAI module not available - return array(); - } - - return $openai->complete_backscroll( $messages ); - } public function slack_gpt_respond_in_thread( $ts, $channel, $response ) { // Convert markdown URLs to Slack format @@ -168,7 +153,7 @@ public function slack_gpt_respond_in_thread( $ts, $channel, $response ) { 'Content-type' => 'application/json; charset=utf-8', 'Authorization' => 'Bearer ' . $this->get_setting( 'api_token' ), ), - 'body' => json_encode( $data ), + 'body' => wp_json_encode( $data ), ) ); @@ -180,9 +165,30 @@ public function pos_process_slack_callback( array $payload ): void { return; } // TODO: This is a hack and instead user should connect to slack and get the user id via Oauth. - POS::get_module_by_id( 'notes' )->switch_to_user(); + $notes = POS::get_module_by_id( 'notes' ); + $notes->switch_to_user(); $this->log( 'pos_process_slack_callback:' . wp_json_encode( $payload ) ); $backscroll = $this->slack_gpt_retrieve_backscroll( $payload['event']['thread_ts'] ?? $payload['event']['ts'], $payload['event']['channel'] ); + + // @TODO: Make UX to match this on the frontend? + // @TODO: Add starter content? + $matching_prompts = $notes->list( + array( + 'meta_query' => array( + array( + 'key' => 'slack_channel_id', + 'value' => $payload['event']['channel'], + ), + ), + ), + 'prompts' + ); + if ( empty( $matching_prompts ) ) { + $prompt = null; + } else { + $prompt = $matching_prompts[ array_rand( $matching_prompts ) ]; + } + $backscroll = array_map( function( $message ) { $message['old'] = true; @@ -190,7 +196,12 @@ function( $message ) { }, $backscroll ); - $response = $this->process_chat_completion( $backscroll ); + $openai = POS::get_module_by_id( 'openai' ); + if ( ! $openai || ! method_exists( $openai, 'complete_backscroll' ) ) { + $this->log( 'OpenAI module not found' ); + return; + } + $response = $openai->complete_backscroll( $backscroll, null, $prompt ); foreach ( $response as $message ) { if ( ! is_array( $message ) && $message->role === 'assistant' ) { $this->slack_gpt_respond_in_thread( $payload['event']['thread_ts'] ?? $payload['event']['ts'], $payload['event']['channel'], $message->content );