diff --git a/docs/en/changes/changes.md b/docs/en/changes/changes.md index a57c45d225e9..ab45e9646527 100644 --- a/docs/en/changes/changes.md +++ b/docs/en/changes/changes.md @@ -18,6 +18,7 @@ * Fix a minute of metrics on BanyanDB losing the points of an earlier persistence round. `PersistenceTimer` counted the delay to the next round from the moment a round's writes were queued, not answered: the per-worker future completed when `batchDAO.flush` returned, the flush's own future was used only to close a latency histogram, and `endOfFlush` ran after that. A metric enters the persistence session cache only when the storage acknowledges its write, and the BanyanDB bulk processor holds a queued write for up to `flushInterval` (15s) before sending it, so under the 25s `persistentPeriod` an acknowledgement that lagged ten seconds, as it can on a BanyanDB still busy with the boot's schema installs, let the next round find the metric absent from the cache, take the minute for a new one and write it again with only its own points; a measure write replaces the series at that time, so the earlier points were gone. A round now completes when every request of it has been answered and the next round's delay counts from there, and `endOfFlush`, which already forced the Elasticsearch bulk out, now forces the BanyanDB measure bulk out too, so the wait is one round trip and not the bulk's interval. Observed as the first minute of a burst of OTLP metrics vanishing right after an OAP boot. * Fix BanyanDB bulk writes leaving skipped requests' futures pending when a write fails to build. Complete every unfinished request in the aborted batch exceptionally, so persistence rounds and runtime-rule removal can finish with an error instead of waiting indefinitely. * Apply the configured max request header size to HTTP/2 as well. `httpMaxRequestHeaderSize` (and the AWS Firehose receiver's `maxRequestHeaderSize`) only set Armeria's `http1MaxHeaderSize`, so an HTTP/2 client kept the framework default header list size and the setting had no effect on the protocol the OTLP and gRPC-Web clients actually use. The value is now also applied as `http2MaxHeaderListSize`; `0`, which means unlimited on the HTTP/1 side, leaves the HTTP/2 default in place because Armeria rejects a non-positive header list size. +* Add a `none` provider to the `ai-agent-conversation` module, so the AI agent conversation feature can be turned off. The GraphQL query module requires the module, so the `-` selector cannot remove it; `SW_AI_AGENT_CONVERSATION=none` keeps the `ai_agent_session_data` and `ai_agent_session_flow` models out of the storage, answers every conversation query empty and registers no view route. #### UI * Add a Virtual GenAI evaluation-record page and evaluation-score chart in Horizon UI, so operators can inspect evaluation result, level, reason, judge model, timestamp, trace linkage, and the `gen_ai_model_evaluation_score_ppm` trend for evaluated records. diff --git a/docs/en/setup/backend/ai-agent-conversation.md b/docs/en/setup/backend/ai-agent-conversation.md index d04e444be7e1..95689e9278f6 100644 --- a/docs/en/setup/backend/ai-agent-conversation.md +++ b/docs/en/setup/backend/ai-agent-conversation.md @@ -131,6 +131,7 @@ The conversation page of the UI makes one call, this route, and nothing else. ```yaml ai-agent-conversation: selector: ${SW_AI_AGENT_CONVERSATION:default} + none: default: fileReadWindow: ${SW_AI_AGENT_CONVERSATION_FILE_READ_WINDOW:16} roundReadWindow: ${SW_AI_AGENT_CONVERSATION_ROUND_READ_WINDOW:16} @@ -149,7 +150,16 @@ ai-agent-conversation: | `maxFileBytes` | the largest file stored, in bytes; a larger one is rejected at ingest and counted under the reason `size`. 15 MiB by default, under BanyanDB's 16 MiB gRPC message limit. The Sessionizer cuts files and rounds at 2 MiB; only a round from before that cut is larger. | | `maxResponseBytes` | the most bytes one window read may answer with, applied to that read alone on a storage that caps a response per call. The BanyanDB client holds every other read to 50 MB; this module's two window reads carry it as a call option on the same connection, so nothing else changes. 100 MiB by default, above sixteen files at the 2 MiB cut with room for files landed whole. For a root of larger files, raise it or lower the windows, so that the window times `maxFileBytes` stays under it; a read over the limit fails as a storage error. | -The GraphQL query module requires this module, so it cannot be disabled while the GraphQL query module is active. +### Turning the feature off + +The GraphQL query module requires this module, so the `-` selector cannot remove it; `SW_AI_AGENT_CONVERSATION=none` +selects the `none` provider instead, which answers `listConversations` and `getConversationRawFiles` with an empty +result and an `errorReason` saying the module is disabled, and registers no conversation view route, so a `GET` on it +is a 404. + +It also disables the two record models, so nothing of the feature reaches the storage: neither table is created, nor +the BanyanDB `recordsAIAgent` group, whose only members they are. A file the bundled LAL rule still verifies is +dropped for want of a record worker; drop `ai-agent` from `SW_LOG_LAL_FILES` as well to skip that work. ## Limits on the path diff --git a/docs/en/setup/backend/configuration-vocabulary.md b/docs/en/setup/backend/configuration-vocabulary.md index 94838a47bc77..1f7799edf923 100644 --- a/docs/en/setup/backend/configuration-vocabulary.md +++ b/docs/en/setup/backend/configuration-vocabulary.md @@ -179,6 +179,7 @@ It divided into several modules, each of which has its own settings. The followi | - | - | viewRequestTimeout | How long one conversation view request may take, in seconds, in place of the HTTP server's default. | SW_AI_AGENT_CONVERSATION_VIEW_REQUEST_TIMEOUT | 120 | | - | - | maxFileBytes | The largest file stored, in bytes; a larger one is rejected at ingest and counted under the reason `size`. Under BanyanDB's 16 MiB gRPC message limit. | SW_AI_AGENT_CONVERSATION_MAX_FILE_BYTES | 15728640 | | - | - | maxResponseBytes | The most bytes one window read may answer with, applied to that read alone where the storage caps a response per call, in place of the BanyanDB client's 50 MB. | SW_AI_AGENT_CONVERSATION_MAX_RESPONSE_BYTES | 104857600 | +| - | none | - | Turns the feature off: the `ai_agent_session_data` and `ai_agent_session_flow` models are not created in the storage, every conversation query answers empty and no conversation view route is registered. The GraphQL query module requires this module, so the `-` selector cannot remove it. | - | - | | event-analyzer | default | Event Analyzer. | SW_EVENT_ANALYZER | default | | | receiver-register | default | gRPC and HTTPRestful services that provide service, service instance and endpoint register. | - | - | | | receiver-trace | default | gRPC and HTTPRestful services that accept SkyWalking format traces. | - | - | | diff --git a/oap-server/analyzer/ai-agent-conversation/src/main/java/org/apache/skywalking/oap/server/ai/agent/conversation/NoneAIAgentConversationProvider.java b/oap-server/analyzer/ai-agent-conversation/src/main/java/org/apache/skywalking/oap/server/ai/agent/conversation/NoneAIAgentConversationProvider.java new file mode 100644 index 000000000000..24f553813899 --- /dev/null +++ b/oap-server/analyzer/ai-agent-conversation/src/main/java/org/apache/skywalking/oap/server/ai/agent/conversation/NoneAIAgentConversationProvider.java @@ -0,0 +1,75 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package org.apache.skywalking.oap.server.ai.agent.conversation; + +import org.apache.skywalking.oap.server.ai.agent.conversation.query.IConversationQueryService; +import org.apache.skywalking.oap.server.ai.agent.conversation.query.NoneConversationQueryService; +import org.apache.skywalking.oap.server.core.analysis.DisableRegister; +import org.apache.skywalking.oap.server.core.analysis.manual.aiagent.AIAgentSessionDataRecord; +import org.apache.skywalking.oap.server.core.analysis.manual.aiagent.AIAgentSessionFlowRecord; +import org.apache.skywalking.oap.server.library.module.ModuleConfig; +import org.apache.skywalking.oap.server.library.module.ModuleDefine; +import org.apache.skywalking.oap.server.library.module.ModuleProvider; +import org.apache.skywalking.oap.server.library.module.ServiceNotProvidedException; + +/** + * The module doing nothing: no model is created in the storage, no file is stored, no query is answered and the + * conversation view route is not registered. The module cannot be removed with the - selector, + * because the GraphQL query module requires it, so this provider is how the feature is turned off. + */ +public class NoneAIAgentConversationProvider extends ModuleProvider { + @Override + public String name() { + return "none"; + } + + @Override + public Class module() { + return AIAgentConversationModule.class; + } + + @Override + public ConfigCreator newConfigCreator() { + return null; + } + + @Override + public void prepare() throws ServiceNotProvidedException { + // The core module scans @Stream in its start(), after every provider's prepare(), so the two models are + // disabled here: their tables are never created, nor the BanyanDB group they are the only members of. + // A record with no worker is dropped by RecordStreamProcessor, so the LAL rule stores nothing either; + // drop `ai-agent` from SW_LOG_LAL_FILES to skip verifying the files it still parses. + DisableRegister.INSTANCE.add(AIAgentSessionDataRecord.INDEX_NAME); + DisableRegister.INSTANCE.add(AIAgentSessionFlowRecord.INDEX_NAME); + registerServiceImplementation(IConversationQueryService.class, new NoneConversationQueryService()); + } + + @Override + public void start() throws ServiceNotProvidedException { + } + + @Override + public void notifyAfterCompleted() throws ServiceNotProvidedException { + } + + @Override + public String[] requiredModules() { + return new String[0]; + } +} diff --git a/oap-server/analyzer/ai-agent-conversation/src/main/java/org/apache/skywalking/oap/server/ai/agent/conversation/query/NoneConversationQueryService.java b/oap-server/analyzer/ai-agent-conversation/src/main/java/org/apache/skywalking/oap/server/ai/agent/conversation/query/NoneConversationQueryService.java new file mode 100644 index 000000000000..0ba89754647a --- /dev/null +++ b/oap-server/analyzer/ai-agent-conversation/src/main/java/org/apache/skywalking/oap/server/ai/agent/conversation/query/NoneConversationQueryService.java @@ -0,0 +1,67 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package org.apache.skywalking.oap.server.ai.agent.conversation.query; + +import java.util.List; +import java.util.Map; +import javax.annotation.Nullable; +import org.apache.skywalking.oap.server.ai.agent.conversation.query.type.ConversationList; +import org.apache.skywalking.oap.server.ai.agent.conversation.query.type.ConversationRawFiles; +import org.apache.skywalking.oap.server.core.query.input.Duration; + +/** + * Answers every query of a disabled module with nothing, so that the GraphQL query module, which requires the + * {@link org.apache.skywalking.oap.server.ai.agent.conversation.AIAgentConversationModule}, still boots and its + * two conversation queries answer instead of failing. + */ +public class NoneConversationQueryService implements IConversationQueryService { + private static final String DISABLED = + "The ai-agent-conversation module is disabled, its selector is none."; + + @Override + public ConversationList listConversations(final String serviceId, + @Nullable final String serviceInstanceId, + @Nullable final String conversation, + @Nullable final String title, + final Duration duration, + @Nullable final Integer limit) { + final ConversationList list = new ConversationList(); + list.setErrorReason(DISABLED); + return list; + } + + @Nullable + @Override + public Map buildConversationView(final String serviceId, + @Nullable final String serviceInstanceId, + final String conversation) { + return null; + } + + @Override + public ConversationRawFiles getConversationRawFiles(final String serviceId, + @Nullable final String serviceInstanceId, + final String conversation, + @Nullable final List files, + final boolean includeBody) { + final ConversationRawFiles rawFiles = new ConversationRawFiles(); + rawFiles.setErrorReason(DISABLED); + return rawFiles; + } +} diff --git a/oap-server/analyzer/ai-agent-conversation/src/main/resources/META-INF/services/org.apache.skywalking.oap.server.library.module.ModuleProvider b/oap-server/analyzer/ai-agent-conversation/src/main/resources/META-INF/services/org.apache.skywalking.oap.server.library.module.ModuleProvider index 8efb6f92bcbf..670c0faffef1 100644 --- a/oap-server/analyzer/ai-agent-conversation/src/main/resources/META-INF/services/org.apache.skywalking.oap.server.library.module.ModuleProvider +++ b/oap-server/analyzer/ai-agent-conversation/src/main/resources/META-INF/services/org.apache.skywalking.oap.server.library.module.ModuleProvider @@ -16,3 +16,4 @@ # org.apache.skywalking.oap.server.ai.agent.conversation.AIAgentConversationProvider +org.apache.skywalking.oap.server.ai.agent.conversation.NoneAIAgentConversationProvider diff --git a/oap-server/analyzer/ai-agent-conversation/src/test/java/org/apache/skywalking/oap/server/ai/agent/conversation/NoneAIAgentConversationProviderTest.java b/oap-server/analyzer/ai-agent-conversation/src/test/java/org/apache/skywalking/oap/server/ai/agent/conversation/NoneAIAgentConversationProviderTest.java new file mode 100644 index 000000000000..7558d0b8216a --- /dev/null +++ b/oap-server/analyzer/ai-agent-conversation/src/test/java/org/apache/skywalking/oap/server/ai/agent/conversation/NoneAIAgentConversationProviderTest.java @@ -0,0 +1,61 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package org.apache.skywalking.oap.server.ai.agent.conversation; + +import java.util.Collections; +import org.apache.skywalking.oap.server.ai.agent.conversation.query.IConversationQueryService; +import org.apache.skywalking.oap.server.core.analysis.DisableRegister; +import org.apache.skywalking.oap.server.core.analysis.manual.aiagent.AIAgentSessionDataRecord; +import org.apache.skywalking.oap.server.core.analysis.manual.aiagent.AIAgentSessionFlowRecord; +import org.apache.skywalking.oap.server.core.query.input.Duration; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class NoneAIAgentConversationProviderTest { + @Test + public void testTheDisabledModuleStoresAndAnswersNothing() throws Exception { + final NoneAIAgentConversationProvider provider = new NoneAIAgentConversationProvider(); + assertEquals("none", provider.name()); + assertEquals(AIAgentConversationModule.class, provider.module()); + assertNull(provider.newConfigCreator()); + assertEquals(0, provider.requiredModules().length); + + provider.prepare(); + + // Without the models the storage creates neither table, and RecordStreamProcessor has no worker to + // dispatch a file to. + assertTrue(DisableRegister.INSTANCE.include(AIAgentSessionDataRecord.INDEX_NAME)); + assertTrue(DisableRegister.INSTANCE.include(AIAgentSessionFlowRecord.INDEX_NAME)); + + final IConversationQueryService service = provider.getService(IConversationQueryService.class); + assertNotNull(service); + assertTrue(service.listConversations("1", null, null, null, new Duration(), null) + .getConversations() + .isEmpty()); + assertNotNull(service.listConversations("1", null, null, null, new Duration(), null).getErrorReason()); + assertNull(service.buildConversationView("1", null, "c")); + assertTrue(service.getConversationRawFiles("1", null, "c", Collections.emptyList(), true) + .getFiles() + .isEmpty()); + } +} diff --git a/oap-server/server-starter/src/main/resources/application.yml b/oap-server/server-starter/src/main/resources/application.yml index d85a83626bfb..13c6258c95fd 100644 --- a/oap-server/server-starter/src/main/resources/application.yml +++ b/oap-server/server-starter/src/main/resources/application.yml @@ -270,6 +270,9 @@ ai-evaluation: # AI_AGENT layer). ai-agent-conversation: selector: ${SW_AI_AGENT_CONVERSATION:default} + # Turns the feature off: neither record model is created in the storage, no query is answered and no + # conversation view route is registered. The GraphQL query module requires this module, so `-` can't remove it. + none: default: # How many Session Data files one storage read fetches; keeps a single BanyanDB response under its inbound cap. fileReadWindow: ${SW_AI_AGENT_CONVERSATION_FILE_READ_WINDOW:16}