Skip to content

Commit d6af4f8

Browse files
authored
Merge pull request #57 from tarosky/feature/conversation-history
対話履歴の永続化と質問マイニング (#51)
2 parents b59a376 + f03815b commit d6af4f8

8 files changed

Lines changed: 565 additions & 10 deletions

File tree

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,7 @@ You can contribute to our github repo. Any [issues](https://github.com/tarosky/h
112112
- Change ownership to Tarosky.
113113
- AI Overview now supports **multi-turn conversations**. Follow-up questions keep the previous exchanges as context, and answers stack as a Q&A thread. Conversation history is held in the browser and sent with each request, so nothing is stored on the server.
114114
- Add `hamelp_history_window` filter to limit how many prior messages are sent to the LLM (default 10).
115+
- Optionally **save conversations** for question mining (off by default). When enabled on the settings page, conversations are stored as a private post type viewable in the admin, so you can see what visitors actually ask. Toggle via the **Save Conversations** setting or the `hamelp_save_conversations` filter.
115116

116117
### 2.2.3
117118

app/Hametuha/Hamelp/Hooks/AiOverview.php

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
namespace Hametuha\Hamelp\Hooks;
99

1010
use Hametuha\Hamelp\Pattern\Singleton;
11+
use Hametuha\Hamelp\Services\ConversationStore;
1112
use Hametuha\Hamelp\Services\FaqSearchService;
1213

1314
/**
@@ -43,18 +44,24 @@ public function register_routes() {
4344
'callback' => [ $this, 'handle_request' ],
4445
'permission_callback' => [ $this, 'check_permission' ],
4546
'args' => [
46-
'query' => [
47+
'query' => [
4748
'required' => true,
4849
'type' => 'string',
4950
'sanitize_callback' => 'sanitize_text_field',
5051
],
51-
'history' => [
52+
'history' => [
5253
'required' => false,
5354
'type' => 'array',
5455
'default' => [],
5556
// Items are associative arrays (role/content); sanitized
5657
// and windowed in FaqSearchService::prepare_history().
5758
],
59+
'conversation_id' => [
60+
'required' => false,
61+
'type' => 'string',
62+
'default' => '',
63+
'sanitize_callback' => 'sanitize_text_field',
64+
],
5865
],
5966
]
6067
);
@@ -208,6 +215,25 @@ public function handle_request( \WP_REST_Request $request ) {
208215
// Increment rate counters on successful AI call.
209216
$this->increment_rate_counters();
210217

218+
// Persist the exchange when conversation saving is enabled (opt-in).
219+
// Done only after a successful answer so empty attempts are never stored.
220+
$store = new ConversationStore();
221+
if ( $store->is_enabled() ) {
222+
$conversation_id = (string) $request->get_param( 'conversation_id' );
223+
$saved = $store->save_turn(
224+
'' !== $conversation_id ? $conversation_id : null,
225+
$query,
226+
(string) $result['answer'],
227+
$result['cited_ids'] ?? []
228+
);
229+
if ( ! empty( $saved['uuid'] ) ) {
230+
$result['conversation_id'] = $saved['uuid'];
231+
}
232+
}
233+
234+
// cited_ids is internal; do not expose it in the API response.
235+
unset( $result['cited_ids'] );
236+
211237
return rest_ensure_response( $result );
212238
}
213239
}
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
<?php
2+
/**
3+
* Conversation history hook handler.
4+
*
5+
* @package hamelp
6+
*/
7+
8+
namespace Hametuha\Hamelp\Hooks;
9+
10+
use Hametuha\Hamelp\Pattern\Singleton;
11+
use Hametuha\Hamelp\Services\ConversationStore;
12+
13+
/**
14+
* Registers the private conversation post type used for question mining.
15+
*
16+
* The post type is intentionally non-public: it is not queryable on the front
17+
* end and is excluded from search and the REST listing. It is only surfaced in
18+
* wp-admin so site owners can review what visitors actually asked.
19+
*/
20+
class ConversationHistory extends Singleton {
21+
22+
/**
23+
* Initialize hooks.
24+
*/
25+
protected function init() {
26+
add_action( 'init', [ $this, 'register_post_type' ] );
27+
add_filter( 'manage_' . ConversationStore::POST_TYPE . '_posts_columns', [ $this, 'columns' ] );
28+
add_action( 'manage_' . ConversationStore::POST_TYPE . '_posts_custom_column', [ $this, 'render_column' ], 10, 2 );
29+
}
30+
31+
/**
32+
* Register the conversation post type.
33+
*/
34+
public function register_post_type() {
35+
register_post_type(
36+
ConversationStore::POST_TYPE,
37+
[
38+
'label' => __( 'Conversations', 'hamelp' ),
39+
'labels' => [
40+
'name' => __( 'Conversations', 'hamelp' ),
41+
'singular_name' => __( 'Conversation', 'hamelp' ),
42+
'menu_name' => __( 'AI Conversations', 'hamelp' ),
43+
],
44+
'public' => false,
45+
'publicly_queryable' => false,
46+
'exclude_from_search' => true,
47+
'show_ui' => true,
48+
'show_in_menu' => true,
49+
'show_in_rest' => false,
50+
'has_archive' => false,
51+
'rewrite' => false,
52+
'menu_icon' => 'dashicons-format-chat',
53+
'menu_position' => 21,
54+
'supports' => [ 'title', 'editor', 'author' ],
55+
'map_meta_cap' => true,
56+
// Read-only data: prevent creating new conversations by hand.
57+
'capabilities' => [
58+
'create_posts' => 'do_not_allow',
59+
],
60+
]
61+
);
62+
}
63+
64+
/**
65+
* Customize admin list columns.
66+
*
67+
* @param array $columns Existing columns.
68+
* @return array
69+
*/
70+
public function columns( $columns ) {
71+
$new = [];
72+
foreach ( $columns as $key => $label ) {
73+
if ( 'title' === $key ) {
74+
$new[ $key ] = __( 'First Question', 'hamelp' );
75+
$new['turns'] = __( 'Turns', 'hamelp' );
76+
} else {
77+
$new[ $key ] = $label;
78+
}
79+
}
80+
return $new;
81+
}
82+
83+
/**
84+
* Render custom column values.
85+
*
86+
* @param string $column Column key.
87+
* @param int $post_id Post ID.
88+
*/
89+
public function render_column( $column, $post_id ) {
90+
if ( 'turns' !== $column ) {
91+
return;
92+
}
93+
$raw = get_post_meta( $post_id, ConversationStore::META_TURNS, true );
94+
$turns = $raw ? json_decode( $raw, true ) : [];
95+
echo esc_html( is_array( $turns ) ? (string) count( $turns ) : '0' );
96+
}
97+
}

app/Hametuha/Hamelp/Hooks/Settings.php

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,36 @@ public function register_settings() {
251251
]
252252
);
253253
}
254+
255+
// Conversation history (opt-in, disabled by default for privacy).
256+
register_setting(
257+
self::OPTION_GROUP,
258+
'hamelp_save_conversations',
259+
[
260+
'type' => 'string',
261+
'sanitize_callback' => 'sanitize_text_field',
262+
'default' => '',
263+
]
264+
);
265+
266+
add_settings_section(
267+
'hamelp_history_section',
268+
__( 'Conversation History', 'hamelp' ),
269+
[ $this, 'render_history_section' ],
270+
self::PAGE_SLUG
271+
);
272+
273+
add_settings_field(
274+
'hamelp_save_conversations',
275+
__( 'Save Conversations', 'hamelp' ),
276+
[ $this, 'render_checkbox' ],
277+
self::PAGE_SLUG,
278+
'hamelp_history_section',
279+
[
280+
'option_name' => 'hamelp_save_conversations',
281+
'description' => __( 'Store AI Overview conversations so you can review what visitors asked. Questions are saved to your database.', 'hamelp' ),
282+
]
283+
);
254284
}
255285

256286
/**
@@ -341,6 +371,16 @@ public function render_rate_section() {
341371
);
342372
}
343373

374+
/**
375+
* Render conversation history section description.
376+
*/
377+
public function render_history_section() {
378+
printf(
379+
'<p>%s</p>',
380+
esc_html__( 'Optionally keep a record of AI Overview conversations for question mining. Automatic deletion and privacy tools will be added in a future release.', 'hamelp' )
381+
);
382+
}
383+
344384
/**
345385
* Render a textarea field.
346386
*

0 commit comments

Comments
 (0)