From f0c26a894c8d5fafec0fb520e3dfa6b27c05fc27 Mon Sep 17 00:00:00 2001 From: Axel Uhl Date: Thu, 6 Mar 2025 17:02:00 +0100 Subject: [PATCH] bug6095: Wiki docs; update Deployment/ChatSession for new Credentials --- Home.md | 1 + .../sailing/aiagent/interfaces/AIAgent.java | 9 +++- .../sap/sailing/aiagent/impl/AIAgentImpl.java | 47 +++++++++++++--- .../sap/sailing/aiagent/impl/Activator.java | 22 +------- java/com.sap.sse.aicore/META-INF/MANIFEST.MF | 1 - .../src/com/sap/sse/aicore/Activator.java | 20 ------- wiki/info/landscape/ai-agent.md | 53 +++++++++++++++++++ 7 files changed, 103 insertions(+), 50 deletions(-) delete mode 100644 java/com.sap.sse.aicore/src/com/sap/sse/aicore/Activator.java create mode 100644 wiki/info/landscape/ai-agent.md diff --git a/Home.md b/Home.md index 50ddecc42b2..e53e193082c 100644 --- a/Home.md +++ b/Home.md @@ -28,6 +28,7 @@ SAP is at the center of today’s 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]] diff --git a/java/com.sap.sailing.aiagent.interfaces/src/com/sap/sailing/aiagent/interfaces/AIAgent.java b/java/com.sap.sailing.aiagent.interfaces/src/com/sap/sailing/aiagent/interfaces/AIAgent.java index e2ed0ca750f..0a401f4961b 100644 --- a/java/com.sap.sailing.aiagent.interfaces/src/com/sap/sailing/aiagent/interfaces/AIAgent.java +++ b/java/com.sap.sailing.aiagent.interfaces/src/com/sap/sailing/aiagent/interfaces/AIAgent.java @@ -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}.

+ * + * 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); } diff --git a/java/com.sap.sailing.aiagent/src/com/sap/sailing/aiagent/impl/AIAgentImpl.java b/java/com.sap.sailing.aiagent/src/com/sap/sailing/aiagent/impl/AIAgentImpl.java index 2c7d8776b21..783aed5f631 100644 --- a/java/com.sap.sailing.aiagent/src/com/sap/sailing/aiagent/impl/AIAgentImpl.java +++ b/java/com.sap.sailing.aiagent/src/com/sap/sailing/aiagent/impl/AIAgentImpl.java @@ -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 racingEventServiceTracker; - private final String modelName; + private final String desiredModelName; private final String systemPrompt; - private final ChatSession chatSession; + private ChatSession chatSession; private final ConcurrentMap 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 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> 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 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; } } diff --git a/java/com.sap.sailing.aiagent/src/com/sap/sailing/aiagent/impl/Activator.java b/java/com.sap.sailing.aiagent/src/com/sap/sailing/aiagent/impl/Activator.java index ed787a594a5..8e0c6ae0a46 100644 --- a/java/com.sap.sailing.aiagent/src/com/sap/sailing/aiagent/impl/Activator.java +++ b/java/com.sap.sailing.aiagent/src/com/sap/sailing/aiagent/impl/Activator.java @@ -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> 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 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 { diff --git a/java/com.sap.sse.aicore/META-INF/MANIFEST.MF b/java/com.sap.sse.aicore/META-INF/MANIFEST.MF index 78a0ac21a1b..7d789ba6839 100644 --- a/java/com.sap.sse.aicore/META-INF/MANIFEST.MF +++ b/java/com.sap.sse.aicore/META-INF/MANIFEST.MF @@ -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 diff --git a/java/com.sap.sse.aicore/src/com/sap/sse/aicore/Activator.java b/java/com.sap.sse.aicore/src/com/sap/sse/aicore/Activator.java deleted file mode 100644 index 6756c722fe9..00000000000 --- a/java/com.sap.sse.aicore/src/com/sap/sse/aicore/Activator.java +++ /dev/null @@ -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; - } -} diff --git a/wiki/info/landscape/ai-agent.md b/wiki/info/landscape/ai-agent.md new file mode 100644 index 00000000000..24977cb7650 --- /dev/null +++ b/wiki/info/landscape/ai-agent.md @@ -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: +``` + + + +``` + +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. \ No newline at end of file