bug6095: Wiki docs; update Deployment/ChatSession for new Credentials

This commit is contained in:
Axel Uhl
2025-03-06 17:02:00 +01:00
parent 446c64aa8b
commit f0c26a894c
7 changed files with 103 additions and 50 deletions
+1
View File
@@ -28,6 +28,7 @@ SAP is at the center of todays technology revolution, developing innovations
* [[Development Environment|wiki/info/landscape/development-environment]]
* [[Production Environment|wiki/info/landscape/production-environment]]
* [[Internationalization (i18n)|wiki/howto/development/i18n]]
* [[AI Agent|wiki/info/landscape/ai-agent]]
* [[Typical Development Scenarios|wiki/info/landscape/typical-development-scenarios]]
* [[RaceLog Tracking Server Architecture|wiki/info/landscape/server]]
* Environment Overview [[PDF|wiki/info/mobile/event-tracking/architecture.pdf]] | [[SVG|wiki/info/mobile/event-tracking/architecture.svg]]
@@ -1,6 +1,8 @@
package com.sap.sailing.aiagent.interfaces;
import com.sap.sailing.domain.base.Event;
import com.sap.sse.aicore.AICore;
import com.sap.sse.aicore.ChatSession;
import com.sap.sse.aicore.Credentials;
public interface AIAgent {
@@ -30,7 +32,12 @@ public interface AIAgent {
* creating the underlying chat session used to generate AI comments for races. Using {@code null}
* will "unset" the credentials, most likely disallowing the use of the service due to
* authentication problems. Also, setting the credentials to {@code null} will let
* {@link #hasCredentials()} return {@code false}.
* {@link #hasCredentials()} return {@code false}.<p>
*
* If non-{@code null} {@code credentials} are provided, a new {@link ChatSession} is created using
* {@link AICore#createChatSession(com.sap.sse.aicore.Deployment)} after resolving the desired model
* name again, defaulting to a model with a default name if with the new credentials the desired
* model cannot be found.
*/
void setCredentials(Credentials credentials);
}
@@ -5,6 +5,7 @@ import java.net.URISyntaxException;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
@@ -44,16 +45,18 @@ import com.sap.sse.shared.util.WeakValueCache;
public class AIAgentImpl implements AIAgent {
private static final Logger logger = Logger.getLogger(AIAgentImpl.class.getName());
private static final String DEFAULT_MODEL_NAME = "gpt-4o-mini";
private static final String SAP_AI_CORE_TAG = "SAP AI Core on %s";
private final ServiceTracker<RacingEventService, RacingEventService> racingEventServiceTracker;
private final String modelName;
private final String desiredModelName;
private final String systemPrompt;
private final ChatSession chatSession;
private ChatSession chatSession;
private final ConcurrentMap<Leaderboard, RaceColumnListener> raceColumnListeners;
@@ -74,22 +77,47 @@ public class AIAgentImpl implements AIAgent {
private final AICore aiCore;
/**
* @param desiredModelName may be {@code null}, leading to the use of a model named according to {@link #DEFAULT_MODEL_NAME}.
*/
public AIAgentImpl(ServiceTracker<RacingEventService, RacingEventService> racingEventServiceTracker, AICore aiCore,
Deployment modelDeployment, String systemPrompt) throws UnsupportedOperationException, ClientProtocolException,
String desiredModelName, String systemPrompt) throws UnsupportedOperationException, ClientProtocolException,
URISyntaxException, IOException, ParseException {
super();
this.aiCore = aiCore;
this.modelName = modelDeployment.getModelName();
this.desiredModelName = desiredModelName;
this.systemPrompt = systemPrompt;
this.tagIdentifiersCurrentlyBeingAddedToRace = new ConcurrentHashMap<>();
this.raceColumnListeners = new ConcurrentHashMap<>();
this.eventListeners = new ConcurrentHashMap<>();
this.racingEventServiceTracker = racingEventServiceTracker;
this.chatSession = aiCore.createChatSession(modelDeployment);
this.chatSession = createChatSession();
this.locks = new WeakValueCache<>(new HashMap<>());
this.listeners = Collections.newSetFromMap(new ConcurrentHashMap<>());
}
/**
* Using the {@link #aiCore} facade to SAP AI Core, obtains the {@link Deployment}s available and tries to find
* one that has the {@link #desiredModelName}. If not, it defaults to a model named according to {@link #DEFAULT_MODEL_NAME}.
* This deployment is then used to create a new {@link ChatSession} which is then returned.
*/
private ChatSession createChatSession() throws UnsupportedOperationException, ClientProtocolException, URISyntaxException, IOException, ParseException {
final String effectiveModelName;
final Map<String, Set<Deployment>> deploymentsByModelName = new HashMap<>();
aiCore.getDeployments().forEach(d->Util.addToValueSet(deploymentsByModelName, d.getModelName(), d));
logger.info("Found AI models "+deploymentsByModelName.keySet());
if (desiredModelName == null || deploymentsByModelName.get(desiredModelName) == null || deploymentsByModelName.get(desiredModelName).isEmpty()) {
logger.warning("Couldn't find model "+desiredModelName+"; defaulting to "+DEFAULT_MODEL_NAME);
effectiveModelName = DEFAULT_MODEL_NAME;
} else {
logger.info("Found model "+desiredModelName);
effectiveModelName = desiredModelName;
}
final Set<Deployment> deployments = deploymentsByModelName.get(effectiveModelName);
final Deployment deployment = deployments.iterator().next();
return aiCore.createChatSession(deployment);
}
@Override
public boolean hasCredentials() {
return aiCore.hasCredentials();
@@ -98,6 +126,11 @@ public class AIAgentImpl implements AIAgent {
@Override
public void setCredentials(Credentials credentials) {
aiCore.setCredentials(credentials);
try {
chatSession = createChatSession();
} catch (UnsupportedOperationException | URISyntaxException | IOException | ParseException e) {
throw new RuntimeException(e);
}
listeners.forEach(l->l.credentialsUpdated(credentials));
}
@@ -118,7 +151,7 @@ public class AIAgentImpl implements AIAgent {
/**
* Checks if a tag with the {@code tagIdentifier} is already found on the race identified by
* {@code leaderboardName}, {@code raceColumnName} and {@code fleetName}; if not, the prompt is sent to a new chat
* session created with the LLM identified by {@link #modelName}, and a new tag is added to that race using the
* session created with the LLM identified by {@link #desiredModelName}, and a new tag is added to that race using the
* response received.
*
* @param tagIdentifier
@@ -318,6 +351,6 @@ public class AIAgentImpl implements AIAgent {
@Override
public String getModelName() {
return modelName;
return desiredModelName;
}
}
@@ -1,8 +1,5 @@
package com.sap.sailing.aiagent.impl;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.logging.Logger;
import org.osgi.framework.BundleActivator;
@@ -17,8 +14,6 @@ import com.sap.sailing.domain.tracking.RaceChangeListener;
import com.sap.sailing.server.interfaces.RacingEventService;
import com.sap.sse.aicore.AICore;
import com.sap.sse.aicore.ChatSession;
import com.sap.sse.aicore.Deployment;
import com.sap.sse.common.Util;
import com.sap.sse.util.ServiceTrackerFactory;
/**
@@ -37,7 +32,6 @@ import com.sap.sse.util.ServiceTrackerFactory;
*/
public class Activator implements BundleActivator {
private static final String MODEL_NAME_SYSTEM_PROPERTY_NAME = "sap.sailing.aiagent.modelname";
private static final String DEFAULT_MODEL_NAME = "gpt-4o-mini";
private static final Logger logger = Logger.getLogger(Activator.class.getName());
@@ -70,21 +64,7 @@ public class Activator implements BundleActivator {
racingEventServiceTracker = ServiceTrackerFactory.createAndOpen(context, RacingEventService.class);
final AICore aiCore = AICore.getDefault();
if (aiCore != null) { // since we now also support AICore instances without Credentials, this case is the most likely
final String modelName = System.getProperty(MODEL_NAME_SYSTEM_PROPERTY_NAME, DEFAULT_MODEL_NAME);
final String effectiveModelName;
final Map<String, Set<Deployment>> deploymentsByModelName = new HashMap<>();
aiCore.getDeployments().forEach(d->Util.addToValueSet(deploymentsByModelName, d.getModelName(), d));
logger.info("Found AI models "+deploymentsByModelName.keySet());
if (deploymentsByModelName.get(modelName) == null || deploymentsByModelName.get(modelName).isEmpty()) {
logger.warning("Couldn't find model "+modelName+"; defaulting to "+DEFAULT_MODEL_NAME);
effectiveModelName = DEFAULT_MODEL_NAME;
} else {
logger.info("Found model "+modelName);
effectiveModelName = modelName;
}
final Set<Deployment> deployments = deploymentsByModelName.get(effectiveModelName);
final Deployment deployment = deployments.iterator().next();
aiAgent = new AIAgentImpl(racingEventServiceTracker, aiCore, deployment, SYSTEM_PROMPT);
aiAgent = new AIAgentImpl(racingEventServiceTracker, aiCore, System.getProperty(MODEL_NAME_SYSTEM_PROPERTY_NAME), SYSTEM_PROMPT);
logger.info("Created AI Agent "+aiAgent);
bundleContext.registerService(AIAgent.class, aiAgent, /* properties */ null);
} else {
@@ -4,7 +4,6 @@ Bundle-Name: Aicore
Bundle-SymbolicName: com.sap.sse.aicore
Bundle-Version: 1.0.0.qualifier
Export-Package: com.sap.sse.aicore
Bundle-Activator: com.sap.sse.aicore.Activator
Bundle-Vendor: SAP
Bundle-RequiredExecutionEnvironment: JavaSE-1.8
Automatic-Module-Name: com.sap.sse.aicore
@@ -1,20 +0,0 @@
package com.sap.sse.aicore;
import org.osgi.framework.BundleActivator;
import org.osgi.framework.BundleContext;
public class Activator implements BundleActivator {
private static BundleContext context;
static BundleContext getContext() {
return context;
}
public void start(BundleContext bundleContext) throws Exception {
Activator.context = bundleContext;
}
public void stop(BundleContext bundleContext) throws Exception {
Activator.context = null;
}
}
+53
View File
@@ -0,0 +1,53 @@
# AI Agent
With the advent of powerful large language models (LLMs), we can now use this technology to produce meaningful comments based on things that happen in races and regattas. We base this feature set on [SAP AI Core](https://help.sap.com/docs/sap-ai-core/sap-ai-core-service-guide/what-is-sap-ai-core) which helps with credentials checking, session management, metering, model selection, and the credits and payment infrastructure behind it. This said, valid AI Core credentials are required for a deployment of the Sailing Analytics to enable this feature set.
## Relevant Bundles
A total of five OSGi runtime bundles (plus a few test fragments) have been added to support this feature set.
### com.sap.sse.aicore
This bundle contains a basic facade for SAP AI Core. It manages credentials and can use them to authorize HTTP requests made against SAP AI Core, can list the models deployed, make model selection, and create chat sessions that can be used with the typical ``chat-completion`` openai REST API. The chat sessions can be parameterized with the typical settings, such as ``temperature`` or ``top_p`` values. Default instances will be created with credentials provided in the ``sap.aicore.credentials`` system property, or otherwise with initially empty credentials.
### com.sap.sailing.aiagent.interfaces
An OSGi "interface" bundle that contains no implementation, only the ``AIAgent`` interface and a corresponding listener interface. This way, other bundles can discover an ``AIAgent`` implementation through the OSGi registry using a service tracker and will survive a refresh of the implementation bundle ``com.sap.sailing.aiagent`` (see below).
### com.sap.sailing.aiagent
The implementation bundle for the ``com.sap.sailing.aiagent.interfaces`` bundle. Its activator constructs a default ``AIAgent`` using a default ``AICore`` instance and registers it with the OSGi registry under the ``AIAgent`` interface.
### com.sap.sailing.aiagent.gateway
Provides a REST API for the AI Agent. This is made available under the ``/aiagent/api/v1/aiagent`` base URL and currently supports methods for starting and stopping the AI commenting for sailing events.
### com.sap.sailing.aiagent.persistence
This bundle implements MongoDB-based persistence for the AI Agent's configuration data, in particular the events currently in commenting mode, and non-standard credentials as provided through the Admin Console. The bundle is configured to start automatically, discovers the ``AIAgent`` service from the OSGi registry and, if found, configures it based on the data read from the database. Additionally, this bundle registeres as a listener on the ``AIAgent`` so that any relevant changes will lead to the corresponding database updates.
## Configuration
The AI Agent feature set is configured by a combination of system properties, through the [Admin Console](/gwt/AdminConsole.html#AIAgentConfigurationPlace:), the REST API, and upon start-up from the database. There are two essential system properties used for the configuration right now:
- ``sap.aicore.credentials``: if provided, its value is expected to be a JSON document with AI Core credentials, as you would get from viewing your credentials of you AI Core instance in SAP BTP Cockpit; if not provided, credentials may still be provided through the Admin Console later.
- ``sap.sailing.aiagent.modelname``: if provided, selected a language model name to use; a model deployment by that name must be available in the AI Core account for which the credentials are valid. If not provided, or if no model deployment by that name can be found, a default model is used (currently gpt-4o-mini). Note that the model you choose this way needs to comply with the _openai_ API for ``chat-completions`` in order to be used in this context.
Upon start-up, the persistence bundle looks for credentials stored persistently which, if found, will override any credentials that may have been provided through the ``sap.aicore.credentials`` system property; furthermore, the set of events for which AI commenting is to be enabled.
## Start-Up Sequence
In ``raceanalysis.product`` we have:
```
<plugin id="com.sap.sailing.aiagent" autoStart="true" startLevel="4" />
<plugin id="com.sap.sailing.aiagent.persistence" autoStart="true" startLevel="5" />
<plugin id="com.sap.sailing.aiagent.gateway" autoStart="true" startLevel="6" />
```
With this, ``com.sap.sailing.aiagent`` starts first, obtains a default ``AICore`` instance which will try to initialize credentials from the ``sap.aicore.credentials`` system property. With this ``AICore`` instance (regardless of whether valid credentials were found through the system property) an ``AIAgent`` implementation instance will be created, using the value of the ``sap.sailing.aiagent.modelname`` system property for the desired model name. If that property isn't set, or if the model specified by that property cannot be resolved using the default credentials from the ``sap.aicore.credentials`` property of the ``AICore`` instance, a default model will be used (currently ``gpt-4o-mini``).
This ``AIAgent`` instance will then be registered with the OSGi registry by the activator of the ``com.sap.sailing.aiagent`` bundle.
The ``com.sap.sailing.aiagent.persistence`` bundle will start up after the ``com.sap.sailing.aiagent`` bundle. It will discover the ``AIAgent`` instance in the OSGi registry where the ``com.sap.sailing.aiagent`` bundle's activator has registered it. The persistence bundle looks for configuration data in MongoDB. It will tell the ``AIAgent`` to start commenting on those (and only those) events whose IDs have been found in the database. Furthermore, if credentials are found in the database, they will be set on the ``AIAgent`` (and transitively on the ``AICore`` facade), which will lead to a new discovery of the language model by the desired or default name (see above) and subsequently the creation of a new chat session with this model deployment.
From there on, the persistence bundle will act as a listener on the ``AIAgent`` service, getting notified when the set of events to comment on changes and updating the database contents accordingly. Likewise, if the credentials are updated, e.g., by the user through the Admin Console, the new credentials are stored to the database where they will serve for ``AICore`` initialization the next time the ``com.sap.sailing.aiagent`` bundle will be started.