From f0db42264f590e475ae57d1d03b35f6afea6e243 Mon Sep 17 00:00:00 2001 From: flemming-it Date: Thu, 7 May 2026 13:18:22 +0200 Subject: [PATCH] feat: SystemAiStatus + AskAi RPCs (v0.4.0) `HubClient.systemAiStatus()` reports whether the hub-internal LLM hook is configured and which provider / privacy mode is active. `HubClient.askAi(prompt)` issues a one-shot prompt and returns the answer or a structured error kind so the UI can map each failure to its inline-fix copy. LoggedEvent gains `detail` upstream (no SDK change required). Signed-off-by: flemming-it --- lib/src/generated/fai/v1/hub.pb.dart | 248 +++++++++++++++++++++++ lib/src/generated/fai/v1/hub.pbgrpc.dart | 47 +++++ lib/src/generated/fai/v1/hub.pbjson.dart | 47 +++++ lib/src/hub_client.dart | 15 ++ pubspec.yaml | 2 +- 5 files changed, 358 insertions(+), 1 deletion(-) diff --git a/lib/src/generated/fai/v1/hub.pb.dart b/lib/src/generated/fai/v1/hub.pb.dart index 4adc934..a105731 100644 --- a/lib/src/generated/fai/v1/hub.pb.dart +++ b/lib/src/generated/fai/v1/hub.pb.dart @@ -2789,6 +2789,254 @@ class CheckUpdateResponse extends $pb.GeneratedMessage { void clearManifestUrl() => $_clearField(9); } +class SystemAiStatusResponse extends $pb.GeneratedMessage { + factory SystemAiStatusResponse({ + $core.bool? enabled, + $core.String? provider, + $core.String? endpoint, + $core.String? model, + $core.String? privacyMode, + $core.String? apiKeyEnv, + }) { + final $result = create(); + if (enabled != null) { + $result.enabled = enabled; + } + if (provider != null) { + $result.provider = provider; + } + if (endpoint != null) { + $result.endpoint = endpoint; + } + if (model != null) { + $result.model = model; + } + if (privacyMode != null) { + $result.privacyMode = privacyMode; + } + if (apiKeyEnv != null) { + $result.apiKeyEnv = apiKeyEnv; + } + return $result; + } + SystemAiStatusResponse._() : super(); + factory SystemAiStatusResponse.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory SystemAiStatusResponse.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'SystemAiStatusResponse', package: const $pb.PackageName(_omitMessageNames ? '' : 'fai.v1'), createEmptyInstance: create) + ..aOB(1, _omitFieldNames ? '' : 'enabled') + ..aOS(2, _omitFieldNames ? '' : 'provider') + ..aOS(3, _omitFieldNames ? '' : 'endpoint') + ..aOS(4, _omitFieldNames ? '' : 'model') + ..aOS(5, _omitFieldNames ? '' : 'privacyMode') + ..aOS(6, _omitFieldNames ? '' : 'apiKeyEnv') + ..hasRequiredFields = false + ; + + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + SystemAiStatusResponse clone() => SystemAiStatusResponse()..mergeFromMessage(this); + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + SystemAiStatusResponse copyWith(void Function(SystemAiStatusResponse) updates) => super.copyWith((message) => updates(message as SystemAiStatusResponse)) as SystemAiStatusResponse; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static SystemAiStatusResponse create() => SystemAiStatusResponse._(); + SystemAiStatusResponse createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static SystemAiStatusResponse getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static SystemAiStatusResponse? _defaultInstance; + + /// True iff `system_llm` is configured AND privacy_mode != off. + @$pb.TagNumber(1) + $core.bool get enabled => $_getBF(0); + @$pb.TagNumber(1) + set enabled($core.bool v) { $_setBool(0, v); } + @$pb.TagNumber(1) + $core.bool hasEnabled() => $_has(0); + @$pb.TagNumber(1) + void clearEnabled() => $_clearField(1); + + /// openai / anthropic / "" — empty when disabled. + @$pb.TagNumber(2) + $core.String get provider => $_getSZ(1); + @$pb.TagNumber(2) + set provider($core.String v) { $_setString(1, v); } + @$pb.TagNumber(2) + $core.bool hasProvider() => $_has(1); + @$pb.TagNumber(2) + void clearProvider() => $_clearField(2); + + /// The endpoint URL the operator configured. Empty when disabled. + @$pb.TagNumber(3) + $core.String get endpoint => $_getSZ(2); + @$pb.TagNumber(3) + set endpoint($core.String v) { $_setString(2, v); } + @$pb.TagNumber(3) + $core.bool hasEndpoint() => $_has(2); + @$pb.TagNumber(3) + void clearEndpoint() => $_clearField(3); + + /// Model identifier. Empty when disabled. + @$pb.TagNumber(4) + $core.String get model => $_getSZ(3); + @$pb.TagNumber(4) + set model($core.String v) { $_setString(3, v); } + @$pb.TagNumber(4) + $core.bool hasModel() => $_has(3); + @$pb.TagNumber(4) + void clearModel() => $_clearField(4); + + /// off / redacted / full — never empty. + @$pb.TagNumber(5) + $core.String get privacyMode => $_getSZ(4); + @$pb.TagNumber(5) + set privacyMode($core.String v) { $_setString(4, v); } + @$pb.TagNumber(5) + $core.bool hasPrivacyMode() => $_has(4); + @$pb.TagNumber(5) + void clearPrivacyMode() => $_clearField(5); + + /// Name of the env var holding the API key (empty = no auth). + /// Studio's settings page renders the *name*; never the value. + @$pb.TagNumber(6) + $core.String get apiKeyEnv => $_getSZ(5); + @$pb.TagNumber(6) + set apiKeyEnv($core.String v) { $_setString(5, v); } + @$pb.TagNumber(6) + $core.bool hasApiKeyEnv() => $_has(5); + @$pb.TagNumber(6) + void clearApiKeyEnv() => $_clearField(6); +} + +class AskAiRequest extends $pb.GeneratedMessage { + factory AskAiRequest({ + $core.String? prompt, + }) { + final $result = create(); + if (prompt != null) { + $result.prompt = prompt; + } + return $result; + } + AskAiRequest._() : super(); + factory AskAiRequest.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory AskAiRequest.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'AskAiRequest', package: const $pb.PackageName(_omitMessageNames ? '' : 'fai.v1'), createEmptyInstance: create) + ..aOS(1, _omitFieldNames ? '' : 'prompt') + ..hasRequiredFields = false + ; + + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + AskAiRequest clone() => AskAiRequest()..mergeFromMessage(this); + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + AskAiRequest copyWith(void Function(AskAiRequest) updates) => super.copyWith((message) => updates(message as AskAiRequest)) as AskAiRequest; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static AskAiRequest create() => AskAiRequest._(); + AskAiRequest createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static AskAiRequest getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static AskAiRequest? _defaultInstance; + + /// The prompt the operator wants answered. Studio assembles + /// this client-side from the audit event being explained + /// and the chosen privacy mode. + @$pb.TagNumber(1) + $core.String get prompt => $_getSZ(0); + @$pb.TagNumber(1) + set prompt($core.String v) { $_setString(0, v); } + @$pb.TagNumber(1) + $core.bool hasPrompt() => $_has(0); + @$pb.TagNumber(1) + void clearPrompt() => $_clearField(1); +} + +class AskAiResponse extends $pb.GeneratedMessage { + factory AskAiResponse({ + $core.String? errorKind, + $core.String? text, + $core.int? latencyMs, + }) { + final $result = create(); + if (errorKind != null) { + $result.errorKind = errorKind; + } + if (text != null) { + $result.text = text; + } + if (latencyMs != null) { + $result.latencyMs = latencyMs; + } + return $result; + } + AskAiResponse._() : super(); + factory AskAiResponse.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory AskAiResponse.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'AskAiResponse', package: const $pb.PackageName(_omitMessageNames ? '' : 'fai.v1'), createEmptyInstance: create) + ..aOS(1, _omitFieldNames ? '' : 'errorKind') + ..aOS(2, _omitFieldNames ? '' : 'text') + ..a<$core.int>(3, _omitFieldNames ? '' : 'latencyMs', $pb.PbFieldType.OU3) + ..hasRequiredFields = false + ; + + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + AskAiResponse clone() => AskAiResponse()..mergeFromMessage(this); + @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') + AskAiResponse copyWith(void Function(AskAiResponse) updates) => super.copyWith((message) => updates(message as AskAiResponse)) as AskAiResponse; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static AskAiResponse create() => AskAiResponse._(); + AskAiResponse createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static AskAiResponse getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static AskAiResponse? _defaultInstance; + + /// Failure category, mapped from `system_llm::SystemLlmError`. + /// Empty on success. Studio surfaces each value with a fix hint. + /// Values: "" (success), "disabled", "env_missing", "network", + /// "http", "parse", "empty_response". + @$pb.TagNumber(1) + $core.String get errorKind => $_getSZ(0); + @$pb.TagNumber(1) + set errorKind($core.String v) { $_setString(0, v); } + @$pb.TagNumber(1) + $core.bool hasErrorKind() => $_has(0); + @$pb.TagNumber(1) + void clearErrorKind() => $_clearField(1); + + /// Human-readable diagnostic / answer. Always populated: + /// either the assistant text on success, or the error detail + /// on failure. + @$pb.TagNumber(2) + $core.String get text => $_getSZ(1); + @$pb.TagNumber(2) + set text($core.String v) { $_setString(1, v); } + @$pb.TagNumber(2) + $core.bool hasText() => $_has(1); + @$pb.TagNumber(2) + void clearText() => $_clearField(2); + + /// Round-trip latency in milliseconds (success only). + @$pb.TagNumber(3) + $core.int get latencyMs => $_getIZ(2); + @$pb.TagNumber(3) + set latencyMs($core.int v) { $_setUnsignedInt32(2, v); } + @$pb.TagNumber(3) + $core.bool hasLatencyMs() => $_has(2); + @$pb.TagNumber(3) + void clearLatencyMs() => $_clearField(3); +} + const _omitFieldNames = $core.bool.fromEnvironment('protobuf.omit_field_names'); const _omitMessageNames = $core.bool.fromEnvironment('protobuf.omit_message_names'); diff --git a/lib/src/generated/fai/v1/hub.pbgrpc.dart b/lib/src/generated/fai/v1/hub.pbgrpc.dart index a5e541e..f2bd092 100644 --- a/lib/src/generated/fai/v1/hub.pbgrpc.dart +++ b/lib/src/generated/fai/v1/hub.pbgrpc.dart @@ -187,6 +187,14 @@ class HubAdminClient extends $grpc.Client { '/fai.v1.HubAdmin/ChannelStatus', ($1.Empty value) => value.writeToBuffer(), ($core.List<$core.int> value) => $0.ChannelStatusResponse.fromBuffer(value)); + static final _$systemAiStatus = $grpc.ClientMethod<$1.Empty, $0.SystemAiStatusResponse>( + '/fai.v1.HubAdmin/SystemAiStatus', + ($1.Empty value) => value.writeToBuffer(), + ($core.List<$core.int> value) => $0.SystemAiStatusResponse.fromBuffer(value)); + static final _$askAi = $grpc.ClientMethod<$0.AskAiRequest, $0.AskAiResponse>( + '/fai.v1.HubAdmin/AskAi', + ($0.AskAiRequest value) => value.writeToBuffer(), + ($core.List<$core.int> value) => $0.AskAiResponse.fromBuffer(value)); HubAdminClient(super.channel, {super.options, super.interceptors}); @@ -283,6 +291,21 @@ class HubAdminClient extends $grpc.Client { $grpc.ResponseFuture<$0.ChannelStatusResponse> channelStatus($1.Empty request, {$grpc.CallOptions? options}) { return $createUnaryCall(_$channelStatus, request, options: options); } + + /// Read-only system-AI status. Lets Studio render the + /// "configure in Settings" affordance when the feature is + /// off, and the privacy-mode badge when it is on. + $grpc.ResponseFuture<$0.SystemAiStatusResponse> systemAiStatus($1.Empty request, {$grpc.CallOptions? options}) { + return $createUnaryCall(_$systemAiStatus, request, options: options); + } + + /// Ask the system AI a one-shot question. Errors carry the + /// failure category (disabled / env-missing / http / parse) + /// so Studio can map them to the inline-fix-hint copy from + /// `docs/architecture/system-ai.md`. + $grpc.ResponseFuture<$0.AskAiResponse> askAi($0.AskAiRequest request, {$grpc.CallOptions? options}) { + return $createUnaryCall(_$askAi, request, options: options); + } } @$pb.GrpcServiceName('fai.v1.HubAdmin') @@ -402,6 +425,20 @@ abstract class HubAdminServiceBase extends $grpc.Service { false, ($core.List<$core.int> value) => $1.Empty.fromBuffer(value), ($0.ChannelStatusResponse value) => value.writeToBuffer())); + $addMethod($grpc.ServiceMethod<$1.Empty, $0.SystemAiStatusResponse>( + 'SystemAiStatus', + systemAiStatus_Pre, + false, + false, + ($core.List<$core.int> value) => $1.Empty.fromBuffer(value), + ($0.SystemAiStatusResponse value) => value.writeToBuffer())); + $addMethod($grpc.ServiceMethod<$0.AskAiRequest, $0.AskAiResponse>( + 'AskAi', + askAi_Pre, + false, + false, + ($core.List<$core.int> value) => $0.AskAiRequest.fromBuffer(value), + ($0.AskAiResponse value) => value.writeToBuffer())); } $async.Future<$0.CapabilityList> listCapabilities_Pre($grpc.ServiceCall $call, $async.Future<$1.Empty> $request) async { @@ -468,6 +505,14 @@ abstract class HubAdminServiceBase extends $grpc.Service { return channelStatus($call, await $request); } + $async.Future<$0.SystemAiStatusResponse> systemAiStatus_Pre($grpc.ServiceCall $call, $async.Future<$1.Empty> $request) async { + return systemAiStatus($call, await $request); + } + + $async.Future<$0.AskAiResponse> askAi_Pre($grpc.ServiceCall $call, $async.Future<$0.AskAiRequest> $request) async { + return askAi($call, await $request); + } + $async.Future<$0.CapabilityList> listCapabilities($grpc.ServiceCall call, $1.Empty request); $async.Future<$0.ModuleInfoResponse> moduleInfo($grpc.ServiceCall call, $0.ModuleInfoRequest request); $async.Future<$0.CheckPermissionResponse> checkPermission($grpc.ServiceCall call, $0.CheckPermissionRequest request); @@ -484,4 +529,6 @@ abstract class HubAdminServiceBase extends $grpc.Service { $async.Future<$0.ServiceList> listServices($grpc.ServiceCall call, $1.Empty request); $async.Future<$0.CheckUpdateResponse> checkUpdate($grpc.ServiceCall call, $1.Empty request); $async.Future<$0.ChannelStatusResponse> channelStatus($grpc.ServiceCall call, $1.Empty request); + $async.Future<$0.SystemAiStatusResponse> systemAiStatus($grpc.ServiceCall call, $1.Empty request); + $async.Future<$0.AskAiResponse> askAi($grpc.ServiceCall call, $0.AskAiRequest request); } diff --git a/lib/src/generated/fai/v1/hub.pbjson.dart b/lib/src/generated/fai/v1/hub.pbjson.dart index e203e2e..612c1e1 100644 --- a/lib/src/generated/fai/v1/hub.pbjson.dart +++ b/lib/src/generated/fai/v1/hub.pbjson.dart @@ -669,3 +669,50 @@ final $typed_data.Uint8List checkUpdateResponseDescriptor = $convert.base64Decod 'Zlc3RSZWFjaGFibGUSFgoGcmVhc29uGAggASgJUgZyZWFzb24SIQoMbWFuaWZlc3RfdXJsGAkg' 'ASgJUgttYW5pZmVzdFVybA=='); +@$core.Deprecated('Use systemAiStatusResponseDescriptor instead') +const SystemAiStatusResponse$json = { + '1': 'SystemAiStatusResponse', + '2': [ + {'1': 'enabled', '3': 1, '4': 1, '5': 8, '10': 'enabled'}, + {'1': 'provider', '3': 2, '4': 1, '5': 9, '10': 'provider'}, + {'1': 'endpoint', '3': 3, '4': 1, '5': 9, '10': 'endpoint'}, + {'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'}, + ], +}; + +/// Descriptor for `SystemAiStatusResponse`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List systemAiStatusResponseDescriptor = $convert.base64Decode( + 'ChZTeXN0ZW1BaVN0YXR1c1Jlc3BvbnNlEhgKB2VuYWJsZWQYASABKAhSB2VuYWJsZWQSGgoIcH' + 'JvdmlkZXIYAiABKAlSCHByb3ZpZGVyEhoKCGVuZHBvaW50GAMgASgJUghlbmRwb2ludBIUCgVt' + 'b2RlbBgEIAEoCVIFbW9kZWwSIQoMcHJpdmFjeV9tb2RlGAUgASgJUgtwcml2YWN5TW9kZRIeCg' + 'thcGlfa2V5X2VudhgGIAEoCVIJYXBpS2V5RW52'); + +@$core.Deprecated('Use askAiRequestDescriptor instead') +const AskAiRequest$json = { + '1': 'AskAiRequest', + '2': [ + {'1': 'prompt', '3': 1, '4': 1, '5': 9, '10': 'prompt'}, + ], +}; + +/// Descriptor for `AskAiRequest`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List askAiRequestDescriptor = $convert.base64Decode( + 'CgxBc2tBaVJlcXVlc3QSFgoGcHJvbXB0GAEgASgJUgZwcm9tcHQ='); + +@$core.Deprecated('Use askAiResponseDescriptor instead') +const AskAiResponse$json = { + '1': 'AskAiResponse', + '2': [ + {'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'}, + ], +}; + +/// Descriptor for `AskAiResponse`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List askAiResponseDescriptor = $convert.base64Decode( + 'Cg1Bc2tBaVJlc3BvbnNlEh0KCmVycm9yX2tpbmQYASABKAlSCWVycm9yS2luZBISCgR0ZXh0GA' + 'IgASgJUgR0ZXh0Eh0KCmxhdGVuY3lfbXMYAyABKA1SCWxhdGVuY3lNcw=='); + diff --git a/lib/src/hub_client.dart b/lib/src/hub_client.dart index 13b0ef4..c855595 100644 --- a/lib/src/hub_client.dart +++ b/lib/src/hub_client.dart @@ -44,6 +44,8 @@ typedef VerifyEventChainResponse = pb.VerifyEventChainResponse; typedef CheckUpdateResponse = pb.CheckUpdateResponse; typedef ChannelStatusResponse = pb.ChannelStatusResponse; typedef ChannelEntry = pb.ChannelEntry; +typedef SystemAiStatusResponse = pb.SystemAiStatusResponse; +typedef AskAiResponse = pb.AskAiResponse; typedef ModuleInfoResponse = pb.ModuleInfoResponse; typedef FlowSummary = pb.FlowSummary; typedef StoreEntry = pb.StoreEntry; @@ -184,6 +186,19 @@ class HubClient { return _admin.channelStatus(Empty()); } + /// Read-only system-AI status. `enabled=false` lets Studio + /// render "configure in Settings" without making a request. + Future systemAiStatus() { + return _admin.systemAiStatus(Empty()); + } + + /// 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)); + } + /// Search the hub's bundled store index. All filters are /// optional; an empty query returns the first [limit] entries /// the index ships with. diff --git a/pubspec.yaml b/pubspec.yaml index 1eb1b03..8dd5ecb 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.3.0 +version: 0.4.0 publish_to: 'none' repository: https://git.flemming.ws/fai/dart-sdk