Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/en/changes/changes.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
12 changes: 11 additions & 1 deletion docs/en/setup/backend/ai-agent-conversation.md
Original file line number Diff line number Diff line change
Expand Up @@ -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}

@kezhenxu94 kezhenxu94 Sep 9, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I’ll leave it to you to decide if we should disable it by default @wu-sheng

none:
default:
fileReadWindow: ${SW_AI_AGENT_CONVERSATION_FILE_READ_WINDOW:16}
roundReadWindow: ${SW_AI_AGENT_CONVERSATION_ROUND_READ_WINDOW:16}
Expand All @@ -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

Expand Down
1 change: 1 addition & 0 deletions docs/en/setup/backend/configuration-vocabulary.md
Original file line number Diff line number Diff line change
Expand Up @@ -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. | - | - | |
Expand Down
Original file line number Diff line number Diff line change
@@ -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 <code>-</code> 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<? extends ModuleDefine> module() {
return AIAgentConversationModule.class;
}

@Override
public ConfigCreator<? extends ModuleConfig> 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];
}
}
Original file line number Diff line number Diff line change
@@ -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<String, Object> 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<String> files,
final boolean includeBody) {
final ConversationRawFiles rawFiles = new ConversationRawFiles();
rawFiles.setErrorReason(DISABLED);
return rawFiles;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,4 @@
#

org.apache.skywalking.oap.server.ai.agent.conversation.AIAgentConversationProvider
org.apache.skywalking.oap.server.ai.agent.conversation.NoneAIAgentConversationProvider
Original file line number Diff line number Diff line change
@@ -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());
}
}
3 changes: 3 additions & 0 deletions oap-server/server-starter/src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand Down
Loading