Spaces:
Sleeping
Sleeping
File size: 4,656 Bytes
f51c224 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 | <?php
defined( 'ABSPATH' ) || exit;
/**
* Token Log — append-only audit of LLM invocations (Board #16).
*
* Each call to an LLM provider (OpenAI today; pluggable) writes one row.
* The row optionally links to a downstream actor-authored comment via
* (comment_kind, comment_id) once the future invocation surface ships
* and starts attributing AI-driven commits to actors.
*
* Two consumers:
* 1. Sara UI — reads usage from the live response (NOT from this table)
* to render per-turn + session totals in the bubble. Session total is
* JS-only; this table is server-side audit.
* 2. Board #23 handoff layer — its filter callback queries
* has_token_for_comment(kind, comment_id) to resolve operator mode.
* Returns true if a row matches, false otherwise. Always deterministic.
*
* Doctrine constraints honored:
* - No cost calculation. No analytics. No replay.
* - Server-side persistence is for cross-session attribution (the #23
* hook). The Sara UI counters remain session-only as locked.
* - Strictly additive: one new table; no existing table mutated.
*/
class SA_Orch_Token_Log {
private static function table(): string {
global $wpdb;
return $wpdb->prefix . SA_ORCH_DB_PREFIX . 'token_log';
}
/**
* Record one LLM invocation. Returns the new row id, or 0 on failure.
*
* @param string $provider e.g. 'openai'
* @param string $model e.g. 'gpt-4o-mini'
* @param array $usage [ prompt_tokens, completion_tokens, total_tokens ]
* @param int $user_id the WP user_id who triggered the call (or 0)
*/
public static function record( string $provider, string $model, array $usage, int $user_id = 0 ): int {
if ( $provider === '' ) return 0;
global $wpdb;
$ok = $wpdb->insert( self::table(), [
'user_id' => $user_id > 0 ? $user_id : null,
'provider' => $provider,
'model' => $model,
'prompt_tokens' => isset( $usage['prompt_tokens'] ) ? (int) $usage['prompt_tokens'] : 0,
'completion_tokens' => isset( $usage['completion_tokens'] ) ? (int) $usage['completion_tokens'] : 0,
'total_tokens' => isset( $usage['total_tokens'] ) ? (int) $usage['total_tokens'] : 0,
'comment_kind' => null,
'comment_id' => null,
'invoked_at' => current_time( 'mysql', true ),
] );
return $ok !== false ? (int) $wpdb->insert_id : 0;
}
/**
* Link a previously-recorded token row to the comment it produced.
* Called by the future invocation surface when an actor-attributed
* comment is committed from a Sara draft.
*
* Idempotent on the row's identity — re-linking just updates the row.
*/
public static function link_to_comment( int $log_id, string $kind, int $comment_id ): bool {
if ( $log_id <= 0 || $comment_id <= 0 ) return false;
$kind = sanitize_key( $kind );
if ( $kind === '' ) return false;
global $wpdb;
$ok = $wpdb->update(
self::table(),
[
'comment_kind' => $kind,
'comment_id' => $comment_id,
],
[ 'id' => $log_id ]
);
return $ok !== false;
}
/**
* Filter callback for Board #23's 'sa_orch_handoff_has_llm_token'.
* Returns true if at least one token row is linked to (kind, comment_id),
* false otherwise. Always deterministic — no nulls.
*/
public static function has_token_for_comment( string $kind, int $comment_id ): bool {
if ( $comment_id <= 0 ) return false;
$kind = sanitize_key( $kind );
if ( $kind === '' ) return false;
global $wpdb;
$count = (int) $wpdb->get_var( $wpdb->prepare(
"SELECT COUNT(*) FROM " . self::table() . "
WHERE comment_kind = %s AND comment_id = %d",
$kind,
$comment_id
) );
return $count > 0;
}
/**
* Bridge to the Board #23 hook. Hooked at plugin load via add_filter.
* Replaces the default 'unknown' (null) with a deterministic boolean.
*
* @param mixed $default The default value (typically null).
* @param string $kind Comment kind constant.
* @param int $comment_id Comment id.
* @return bool
*/
public static function answer_handoff_filter( $default, string $kind, int $comment_id ): bool {
return self::has_token_for_comment( $kind, $comment_id );
}
}
|