From 6dc058344b855a6663daa73d8ccf0e6ceff02e75 Mon Sep 17 00:00:00 2001 From: flemming-it Date: Thu, 7 May 2026 17:29:26 +0200 Subject: [PATCH] feat: System-AI cache surface (v0.8.0) Picks up cache-related additions in HubAdmin: - AskAi gains `forceFresh` request flag and `cached` / `cachedAt` / `cacheHits` response fields. - New ClearSystemLlmCache + ForgetCachedExplanation RPCs. - SystemAiStatus carries `cacheCount` so the UI can render "Cache: N cached explanations". HubClient exposes `clearSystemLlmCache()` returning the purged count and `forgetCachedExplanation(prompt)` for the Regenerate flow. Signed-off-by: flemming-it --- lib/src/generated/fai/v1/hub.pb.dart | 132 ++++++++++++++++++++++- lib/src/generated/fai/v1/hub.pbgrpc.dart | 46 ++++++++ lib/src/generated/fai/v1/hub.pbjson.dart | 27 ++++- lib/src/hub_client.dart | 27 ++++- pubspec.yaml | 2 +- 5 files changed, 227 insertions(+), 7 deletions(-) diff --git a/lib/src/generated/fai/v1/hub.pb.dart b/lib/src/generated/fai/v1/hub.pb.dart index 310d192..e413fb9 100644 --- a/lib/src/generated/fai/v1/hub.pb.dart +++ b/lib/src/generated/fai/v1/hub.pb.dart @@ -2797,6 +2797,7 @@ class SystemAiStatusResponse extends $pb.GeneratedMessage { $core.String? model, $core.String? privacyMode, $core.String? apiKeyEnv, + $fixnum.Int64? cacheCount, }) { final $result = create(); if (enabled != null) { @@ -2817,6 +2818,9 @@ class SystemAiStatusResponse extends $pb.GeneratedMessage { if (apiKeyEnv != null) { $result.apiKeyEnv = apiKeyEnv; } + if (cacheCount != null) { + $result.cacheCount = cacheCount; + } return $result; } SystemAiStatusResponse._() : super(); @@ -2830,6 +2834,7 @@ class SystemAiStatusResponse extends $pb.GeneratedMessage { ..aOS(4, _omitFieldNames ? '' : 'model') ..aOS(5, _omitFieldNames ? '' : 'privacyMode') ..aOS(6, _omitFieldNames ? '' : 'apiKeyEnv') + ..a<$fixnum.Int64>(7, _omitFieldNames ? '' : 'cacheCount', $pb.PbFieldType.OU6, defaultOrMaker: $fixnum.Int64.ZERO) ..hasRequiredFields = false ; @@ -2908,16 +2913,32 @@ class SystemAiStatusResponse extends $pb.GeneratedMessage { $core.bool hasApiKeyEnv() => $_has(5); @$pb.TagNumber(6) void clearApiKeyEnv() => $_clearField(6); + + /// Number of cached System-AI explanations currently stored. + /// Surfaced in Doctor + Settings so operators see the cache + /// earning its keep. + @$pb.TagNumber(7) + $fixnum.Int64 get cacheCount => $_getI64(6); + @$pb.TagNumber(7) + set cacheCount($fixnum.Int64 v) { $_setInt64(6, v); } + @$pb.TagNumber(7) + $core.bool hasCacheCount() => $_has(6); + @$pb.TagNumber(7) + void clearCacheCount() => $_clearField(7); } class AskAiRequest extends $pb.GeneratedMessage { factory AskAiRequest({ $core.String? prompt, + $core.bool? forceFresh, }) { final $result = create(); if (prompt != null) { $result.prompt = prompt; } + if (forceFresh != null) { + $result.forceFresh = forceFresh; + } return $result; } AskAiRequest._() : super(); @@ -2926,6 +2947,7 @@ class AskAiRequest extends $pb.GeneratedMessage { static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'AskAiRequest', package: const $pb.PackageName(_omitMessageNames ? '' : 'fai.v1'), createEmptyInstance: create) ..aOS(1, _omitFieldNames ? '' : 'prompt') + ..aOB(2, _omitFieldNames ? '' : 'forceFresh') ..hasRequiredFields = false ; @@ -2955,6 +2977,18 @@ class AskAiRequest extends $pb.GeneratedMessage { $core.bool hasPrompt() => $_has(0); @$pb.TagNumber(1) void clearPrompt() => $_clearField(1); + + /// When true, skip the persistent cache and always hit the + /// live provider. Studio's "Regenerate" button passes this so + /// the operator can ask the model to think again. + @$pb.TagNumber(2) + $core.bool get forceFresh => $_getBF(1); + @$pb.TagNumber(2) + set forceFresh($core.bool v) { $_setBool(1, v); } + @$pb.TagNumber(2) + $core.bool hasForceFresh() => $_has(1); + @$pb.TagNumber(2) + void clearForceFresh() => $_clearField(2); } class AskAiResponse extends $pb.GeneratedMessage { @@ -2962,6 +2996,9 @@ class AskAiResponse extends $pb.GeneratedMessage { $core.String? errorKind, $core.String? text, $core.int? latencyMs, + $core.bool? cached, + $core.String? cachedAt, + $core.int? cacheHits, }) { final $result = create(); if (errorKind != null) { @@ -2973,6 +3010,15 @@ class AskAiResponse extends $pb.GeneratedMessage { if (latencyMs != null) { $result.latencyMs = latencyMs; } + if (cached != null) { + $result.cached = cached; + } + if (cachedAt != null) { + $result.cachedAt = cachedAt; + } + if (cacheHits != null) { + $result.cacheHits = cacheHits; + } return $result; } AskAiResponse._() : super(); @@ -2983,6 +3029,9 @@ class AskAiResponse extends $pb.GeneratedMessage { ..aOS(1, _omitFieldNames ? '' : 'errorKind') ..aOS(2, _omitFieldNames ? '' : 'text') ..a<$core.int>(3, _omitFieldNames ? '' : 'latencyMs', $pb.PbFieldType.OU3) + ..aOB(4, _omitFieldNames ? '' : 'cached') + ..aOS(5, _omitFieldNames ? '' : 'cachedAt') + ..a<$core.int>(6, _omitFieldNames ? '' : 'cacheHits', $pb.PbFieldType.OU3) ..hasRequiredFields = false ; @@ -3026,7 +3075,10 @@ class AskAiResponse extends $pb.GeneratedMessage { @$pb.TagNumber(2) void clearText() => $_clearField(2); - /// Round-trip latency in milliseconds (success only). + /// Round-trip latency in milliseconds (success only). For + /// cache hits this is the *original* generation latency, not + /// the cache-lookup time — the UI renders it as "originally + /// took X" so the operator sees what a live call would cost. @$pb.TagNumber(3) $core.int get latencyMs => $_getIZ(2); @$pb.TagNumber(3) @@ -3035,6 +3087,84 @@ class AskAiResponse extends $pb.GeneratedMessage { $core.bool hasLatencyMs() => $_has(2); @$pb.TagNumber(3) void clearLatencyMs() => $_clearField(3); + + /// True iff the answer came from the persistent cache rather + /// than a fresh provider round-trip. + @$pb.TagNumber(4) + $core.bool get cached => $_getBF(3); + @$pb.TagNumber(4) + set cached($core.bool v) { $_setBool(3, v); } + @$pb.TagNumber(4) + $core.bool hasCached() => $_has(3); + @$pb.TagNumber(4) + void clearCached() => $_clearField(4); + + /// ISO-8601 timestamp the cached entry was first generated. + /// Empty for fresh answers. + @$pb.TagNumber(5) + $core.String get cachedAt => $_getSZ(4); + @$pb.TagNumber(5) + set cachedAt($core.String v) { $_setString(4, v); } + @$pb.TagNumber(5) + $core.bool hasCachedAt() => $_has(4); + @$pb.TagNumber(5) + void clearCachedAt() => $_clearField(5); + + /// How many times this entry has been served from cache. + /// 0 for fresh answers. + @$pb.TagNumber(6) + $core.int get cacheHits => $_getIZ(5); + @$pb.TagNumber(6) + set cacheHits($core.int v) { $_setUnsignedInt32(5, v); } + @$pb.TagNumber(6) + $core.bool hasCacheHits() => $_has(5); + @$pb.TagNumber(6) + void clearCacheHits() => $_clearField(6); +} + +class ClearSystemLlmCacheResponse extends $pb.GeneratedMessage { + factory ClearSystemLlmCacheResponse({ + $fixnum.Int64? purged, + }) { + final $result = create(); + if (purged != null) { + $result.purged = purged; + } + return $result; + } + ClearSystemLlmCacheResponse._() : super(); + factory ClearSystemLlmCacheResponse.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory ClearSystemLlmCacheResponse.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'ClearSystemLlmCacheResponse', package: const $pb.PackageName(_omitMessageNames ? '' : 'fai.v1'), createEmptyInstance: create) + ..a<$fixnum.Int64>(1, _omitFieldNames ? '' : 'purged', $pb.PbFieldType.OU6, defaultOrMaker: $fixnum.Int64.ZERO) + ..hasRequiredFields = false + ; + + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + ClearSystemLlmCacheResponse clone() => ClearSystemLlmCacheResponse()..mergeFromMessage(this); + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + ClearSystemLlmCacheResponse copyWith(void Function(ClearSystemLlmCacheResponse) updates) => super.copyWith((message) => updates(message as ClearSystemLlmCacheResponse)) as ClearSystemLlmCacheResponse; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static ClearSystemLlmCacheResponse create() => ClearSystemLlmCacheResponse._(); + ClearSystemLlmCacheResponse createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static ClearSystemLlmCacheResponse getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static ClearSystemLlmCacheResponse? _defaultInstance; + + /// Number of cached entries that were dropped. + @$pb.TagNumber(1) + $fixnum.Int64 get purged => $_getI64(0); + @$pb.TagNumber(1) + set purged($fixnum.Int64 v) { $_setInt64(0, v); } + @$pb.TagNumber(1) + $core.bool hasPurged() => $_has(0); + @$pb.TagNumber(1) + void clearPurged() => $_clearField(1); } class UpdateSystemAiRequest extends $pb.GeneratedMessage { diff --git a/lib/src/generated/fai/v1/hub.pbgrpc.dart b/lib/src/generated/fai/v1/hub.pbgrpc.dart index 4f9bb72..ae53106 100644 --- a/lib/src/generated/fai/v1/hub.pbgrpc.dart +++ b/lib/src/generated/fai/v1/hub.pbgrpc.dart @@ -215,6 +215,14 @@ class HubAdminClient extends $grpc.Client { '/fai.v1.HubAdmin/PullSystemAiModel', ($0.PullSystemAiModelRequest value) => value.writeToBuffer(), ($core.List<$core.int> value) => $0.PullSystemAiModelResponse.fromBuffer(value)); + static final _$clearSystemLlmCache = $grpc.ClientMethod<$1.Empty, $0.ClearSystemLlmCacheResponse>( + '/fai.v1.HubAdmin/ClearSystemLlmCache', + ($1.Empty value) => value.writeToBuffer(), + ($core.List<$core.int> value) => $0.ClearSystemLlmCacheResponse.fromBuffer(value)); + static final _$forgetCachedExplanation = $grpc.ClientMethod<$0.AskAiRequest, $1.Empty>( + '/fai.v1.HubAdmin/ForgetCachedExplanation', + ($0.AskAiRequest value) => value.writeToBuffer(), + ($core.List<$core.int> value) => $1.Empty.fromBuffer(value)); static final _$hardwareInfo = $grpc.ClientMethod<$1.Empty, $0.HardwareInfoResponse>( '/fai.v1.HubAdmin/HardwareInfo', ($1.Empty value) => value.writeToBuffer(), @@ -376,6 +384,20 @@ class HubAdminClient extends $grpc.Client { return $createUnaryCall(_$pullSystemAiModel, request, options: options); } + /// Drop every cached System-AI explanation. Surfaced in + /// Studio's Settings so operators can force a clean slate + /// without changing model/privacy mode (which also flushes). + $grpc.ResponseFuture<$0.ClearSystemLlmCacheResponse> clearSystemLlmCache($1.Empty request, {$grpc.CallOptions? options}) { + return $createUnaryCall(_$clearSystemLlmCache, request, options: options); + } + + /// Drop a single cached entry by prompt — the same prompt + /// Studio sent in AskAi. Used by the "Regenerate" button so + /// the next AskAi call falls through to the live provider. + $grpc.ResponseFuture<$1.Empty> forgetCachedExplanation($0.AskAiRequest request, {$grpc.CallOptions? options}) { + return $createUnaryCall(_$forgetCachedExplanation, request, options: options); + } + /// Detected host hardware tier + summary line. Studio's /// System-AI editor uses this to mark models as "may be slow /// on your hardware" without requiring a benchmark. @@ -559,6 +581,20 @@ abstract class HubAdminServiceBase extends $grpc.Service { false, ($core.List<$core.int> value) => $0.PullSystemAiModelRequest.fromBuffer(value), ($0.PullSystemAiModelResponse value) => value.writeToBuffer())); + $addMethod($grpc.ServiceMethod<$1.Empty, $0.ClearSystemLlmCacheResponse>( + 'ClearSystemLlmCache', + clearSystemLlmCache_Pre, + false, + false, + ($core.List<$core.int> value) => $1.Empty.fromBuffer(value), + ($0.ClearSystemLlmCacheResponse value) => value.writeToBuffer())); + $addMethod($grpc.ServiceMethod<$0.AskAiRequest, $1.Empty>( + 'ForgetCachedExplanation', + forgetCachedExplanation_Pre, + false, + false, + ($core.List<$core.int> value) => $0.AskAiRequest.fromBuffer(value), + ($1.Empty value) => value.writeToBuffer())); $addMethod($grpc.ServiceMethod<$1.Empty, $0.HardwareInfoResponse>( 'HardwareInfo', hardwareInfo_Pre, @@ -667,6 +703,14 @@ abstract class HubAdminServiceBase extends $grpc.Service { return pullSystemAiModel($call, await $request); } + $async.Future<$0.ClearSystemLlmCacheResponse> clearSystemLlmCache_Pre($grpc.ServiceCall $call, $async.Future<$1.Empty> $request) async { + return clearSystemLlmCache($call, await $request); + } + + $async.Future<$1.Empty> forgetCachedExplanation_Pre($grpc.ServiceCall $call, $async.Future<$0.AskAiRequest> $request) async { + return forgetCachedExplanation($call, await $request); + } + $async.Future<$0.HardwareInfoResponse> hardwareInfo_Pre($grpc.ServiceCall $call, $async.Future<$1.Empty> $request) async { return hardwareInfo($call, await $request); } @@ -698,6 +742,8 @@ abstract class HubAdminServiceBase extends $grpc.Service { $async.Future<$0.AskAiResponse> testSystemAi($grpc.ServiceCall call, $0.TestSystemAiRequest request); $async.Future<$0.ListSystemAiModelsResponse> listSystemAiModels($grpc.ServiceCall call, $0.TestSystemAiRequest request); $async.Future<$0.PullSystemAiModelResponse> pullSystemAiModel($grpc.ServiceCall call, $0.PullSystemAiModelRequest request); + $async.Future<$0.ClearSystemLlmCacheResponse> clearSystemLlmCache($grpc.ServiceCall call, $1.Empty request); + $async.Future<$1.Empty> forgetCachedExplanation($grpc.ServiceCall call, $0.AskAiRequest request); $async.Future<$0.HardwareInfoResponse> hardwareInfo($grpc.ServiceCall call, $1.Empty request); $async.Future<$0.ListSystemAiCuratedModelsResponse> listSystemAiCuratedModels($grpc.ServiceCall call, $1.Empty request); } diff --git a/lib/src/generated/fai/v1/hub.pbjson.dart b/lib/src/generated/fai/v1/hub.pbjson.dart index a0ad60d..cc347fb 100644 --- a/lib/src/generated/fai/v1/hub.pbjson.dart +++ b/lib/src/generated/fai/v1/hub.pbjson.dart @@ -679,6 +679,7 @@ const SystemAiStatusResponse$json = { {'1': 'model', '3': 4, '4': 1, '5': 9, '10': 'model'}, {'1': 'privacy_mode', '3': 5, '4': 1, '5': 9, '10': 'privacyMode'}, {'1': 'api_key_env', '3': 6, '4': 1, '5': 9, '10': 'apiKeyEnv'}, + {'1': 'cache_count', '3': 7, '4': 1, '5': 4, '10': 'cacheCount'}, ], }; @@ -687,19 +688,22 @@ final $typed_data.Uint8List systemAiStatusResponseDescriptor = $convert.base64De 'ChZTeXN0ZW1BaVN0YXR1c1Jlc3BvbnNlEhgKB2VuYWJsZWQYASABKAhSB2VuYWJsZWQSGgoIcH' 'JvdmlkZXIYAiABKAlSCHByb3ZpZGVyEhoKCGVuZHBvaW50GAMgASgJUghlbmRwb2ludBIUCgVt' 'b2RlbBgEIAEoCVIFbW9kZWwSIQoMcHJpdmFjeV9tb2RlGAUgASgJUgtwcml2YWN5TW9kZRIeCg' - 'thcGlfa2V5X2VudhgGIAEoCVIJYXBpS2V5RW52'); + 'thcGlfa2V5X2VudhgGIAEoCVIJYXBpS2V5RW52Eh8KC2NhY2hlX2NvdW50GAcgASgEUgpjYWNo' + 'ZUNvdW50'); @$core.Deprecated('Use askAiRequestDescriptor instead') const AskAiRequest$json = { '1': 'AskAiRequest', '2': [ {'1': 'prompt', '3': 1, '4': 1, '5': 9, '10': 'prompt'}, + {'1': 'force_fresh', '3': 2, '4': 1, '5': 8, '10': 'forceFresh'}, ], }; /// Descriptor for `AskAiRequest`. Decode as a `google.protobuf.DescriptorProto`. final $typed_data.Uint8List askAiRequestDescriptor = $convert.base64Decode( - 'CgxBc2tBaVJlcXVlc3QSFgoGcHJvbXB0GAEgASgJUgZwcm9tcHQ='); + 'CgxBc2tBaVJlcXVlc3QSFgoGcHJvbXB0GAEgASgJUgZwcm9tcHQSHwoLZm9yY2VfZnJlc2gYAi' + 'ABKAhSCmZvcmNlRnJlc2g='); @$core.Deprecated('Use askAiResponseDescriptor instead') const AskAiResponse$json = { @@ -708,13 +712,30 @@ const AskAiResponse$json = { {'1': 'error_kind', '3': 1, '4': 1, '5': 9, '10': 'errorKind'}, {'1': 'text', '3': 2, '4': 1, '5': 9, '10': 'text'}, {'1': 'latency_ms', '3': 3, '4': 1, '5': 13, '10': 'latencyMs'}, + {'1': 'cached', '3': 4, '4': 1, '5': 8, '10': 'cached'}, + {'1': 'cached_at', '3': 5, '4': 1, '5': 9, '10': 'cachedAt'}, + {'1': 'cache_hits', '3': 6, '4': 1, '5': 13, '10': 'cacheHits'}, ], }; /// Descriptor for `AskAiResponse`. Decode as a `google.protobuf.DescriptorProto`. final $typed_data.Uint8List askAiResponseDescriptor = $convert.base64Decode( 'Cg1Bc2tBaVJlc3BvbnNlEh0KCmVycm9yX2tpbmQYASABKAlSCWVycm9yS2luZBISCgR0ZXh0GA' - 'IgASgJUgR0ZXh0Eh0KCmxhdGVuY3lfbXMYAyABKA1SCWxhdGVuY3lNcw=='); + 'IgASgJUgR0ZXh0Eh0KCmxhdGVuY3lfbXMYAyABKA1SCWxhdGVuY3lNcxIWCgZjYWNoZWQYBCAB' + 'KAhSBmNhY2hlZBIbCgljYWNoZWRfYXQYBSABKAlSCGNhY2hlZEF0Eh0KCmNhY2hlX2hpdHMYBi' + 'ABKA1SCWNhY2hlSGl0cw=='); + +@$core.Deprecated('Use clearSystemLlmCacheResponseDescriptor instead') +const ClearSystemLlmCacheResponse$json = { + '1': 'ClearSystemLlmCacheResponse', + '2': [ + {'1': 'purged', '3': 1, '4': 1, '5': 4, '10': 'purged'}, + ], +}; + +/// Descriptor for `ClearSystemLlmCacheResponse`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List clearSystemLlmCacheResponseDescriptor = $convert.base64Decode( + 'ChtDbGVhclN5c3RlbUxsbUNhY2hlUmVzcG9uc2USFgoGcHVyZ2VkGAEgASgEUgZwdXJnZWQ='); @$core.Deprecated('Use updateSystemAiRequestDescriptor instead') const UpdateSystemAiRequest$json = { diff --git a/lib/src/hub_client.dart b/lib/src/hub_client.dart index 927692d..a0dd4ac 100644 --- a/lib/src/hub_client.dart +++ b/lib/src/hub_client.dart @@ -217,8 +217,31 @@ class HubClient { /// One-shot prompt to the configured System AI. Errors come /// back inside [AskAiResponse.errorKind] (not as exceptions) /// so Studio can map each category to its inline-fix copy. - Future askAi(String prompt) { - return _admin.askAi(pb.AskAiRequest(prompt: prompt)); + /// + /// Cache-aware: identical prompts hit the persistent + /// per-(model, privacy_mode, prompt) cache; the response + /// carries `cached: true` plus the original generation + /// latency. Pass [forceFresh] to skip the cache (Studio's + /// Regenerate button). + Future askAi(String prompt, {bool forceFresh = false}) { + return _admin.askAi( + pb.AskAiRequest(prompt: prompt, forceFresh: forceFresh), + ); + } + + /// Drop every cached System-AI explanation. Used by Settings + /// when the operator wants a clean slate without changing + /// model/privacy mode (which would also flush). + Future clearSystemLlmCache() async { + final r = await _admin.clearSystemLlmCache(Empty()); + return r.purged.toInt(); + } + + /// Drop a single cached entry by prompt. Studio's Regenerate + /// button calls this so the next askAi falls through to the + /// live provider. + Future forgetCachedExplanation(String prompt) async { + await _admin.forgetCachedExplanation(pb.AskAiRequest(prompt: prompt)); } /// Persist a new System-AI configuration. Validates + diff --git a/pubspec.yaml b/pubspec.yaml index 028cfa3..54458fe 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: fai_dart_sdk description: gRPC client SDK for the F∆I Platform hub. Used by F∆I Studio and Tier-3 domain apps. -version: 0.7.1 +version: 0.8.0 publish_to: 'none' repository: https://git.flemming.ws/fai/dart-sdk