Skip to content

Commit 688b1e7

Browse files
committed
Allow intro message for new dialogues
1 parent e1d08dc commit 688b1e7

File tree

2 files changed

+62
-35
lines changed

2 files changed

+62
-35
lines changed

bot/connector-google-chat/src/main/kotlin/GoogleChatConnector.kt

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import ai.tock.bot.engine.action.Action
3232
import ai.tock.bot.engine.event.Event
3333
import ai.tock.shared.Executor
3434
import ai.tock.shared.injector
35+
import ai.tock.shared.longProperty
3536
import com.github.salomonbrys.kodein.instance
3637
import com.google.api.client.json.jackson2.JacksonFactory
3738
import com.google.api.services.chat.v1.HangoutsChat
@@ -45,11 +46,15 @@ class GoogleChatConnector(
4546
private val path: String,
4647
private val chatService: HangoutsChat,
4748
private val authorisationHandler: GoogleChatAuthorisationHandler,
48-
private val useCondensedFootnotes: Boolean
49+
private val useCondensedFootnotes: Boolean,
50+
private val introMessage: String? = null
4951
) : ConnectorBase(GoogleChatConnectorProvider.connectorType) {
5052

5153
private val logger = KotlinLogging.logger {}
5254
private val executor: Executor by injector.instance()
55+
private val introCooldownSeconds: Long = longProperty("tock_bot_dialog_max_validity_in_seconds", 60)
56+
// Last intro send time per space + thread
57+
private val introSentThreads: MutableMap<String, Long> = mutableMapOf()
5358

5459
override fun register(controller: ConnectorController) {
5560
controller.registerServices(path) { router ->
@@ -87,6 +92,49 @@ class GoogleChatConnector(
8792
override fun send(event: Event, callback: ConnectorCallback, delayInMs: Long) {
8893
logger.debug { "event: $event" }
8994
if (event is Action) {
95+
96+
if (introMessage != null && callback is GoogleChatConnectorCallback) {
97+
val threadKey = callback.spaceName + "|" + callback.threadName
98+
val now = System.currentTimeMillis()
99+
val cooldownMillis = introCooldownSeconds * 1000
100+
101+
val shouldSendIntro = synchronized(introSentThreads) {
102+
val last = introSentThreads[threadKey] ?: 0L
103+
if (now - last >= cooldownMillis) {
104+
introSentThreads[threadKey] = now
105+
true
106+
} else {
107+
false
108+
}
109+
}
110+
111+
if (shouldSendIntro) {
112+
executor.executeBlocking {
113+
try {
114+
logger.info {
115+
"Sending intro message to Google Chat: space=${callback.spaceName}, thread=${callback.threadName}"
116+
}
117+
val introResponse = chatService
118+
.spaces()
119+
.messages()
120+
.create(
121+
callback.spaceName,
122+
GoogleChatConnectorTextMessageOut(introMessage).toGoogleMessage()
123+
.setThread(Thread().setName(callback.threadName))
124+
)
125+
.setMessageReplyOption("REPLY_MESSAGE_FALLBACK_TO_NEW_THREAD")
126+
.execute()
127+
128+
logger.info { "Google Chat API intro response: ${introResponse?.name}" }
129+
} catch (e: Exception) {
130+
logger.error(e) {
131+
"Failed to send intro message to Google Chat (space=${callback.spaceName}, thread=${callback.threadName})"
132+
}
133+
}
134+
}
135+
}
136+
}
137+
90138
val message = GoogleChatMessageConverter.toMessageOut(event, useCondensedFootnotes)
91139
if (message != null) {
92140
callback as GoogleChatConnectorCallback

bot/connector-google-chat/src/main/kotlin/GoogleChatConnectorProvider.kt

Lines changed: 13 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,6 @@
1-
/*
2-
* Copyright (C) 2017/2025 SNCF Connect & Tech
3-
*
4-
* Licensed under the Apache License, Version 2.0 (the "License");
5-
* you may not use this file except in compliance with the License.
6-
* You may obtain a copy of the License at
7-
*
8-
* http://www.apache.org/licenses/LICENSE-2.0
9-
*
10-
* Unless required by applicable law or agreed to in writing, software
11-
* distributed under the License is distributed on an "AS IS" BASIS,
12-
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13-
* See the License for the specific language governing permissions and
14-
* limitations under the License.
15-
*/
161
package ai.tock.bot.connector.googlechat
172

18-
import ai.tock.bot.connector.Connector
19-
import ai.tock.bot.connector.ConnectorConfiguration
20-
import ai.tock.bot.connector.ConnectorMessage
21-
import ai.tock.bot.connector.ConnectorProvider
22-
import ai.tock.bot.connector.ConnectorType
23-
import ai.tock.bot.connector.ConnectorTypeConfiguration
24-
import ai.tock.bot.connector.ConnectorTypeConfigurationField
3+
import ai.tock.bot.connector.*
254
import ai.tock.bot.connector.googlechat.builder.googleChatConnectorType
265
import ai.tock.shared.resourceAsStream
276
import ai.tock.shared.resourceAsString
@@ -44,6 +23,7 @@ private const val SERVICE_CREDENTIAL_CONTENT_PARAMETER = "serviceCredentialConte
4423
private const val BOT_PROJECT_NUMBER_PARAMETER = "botProjectNumber"
4524
private const val CONDENSED_FOOTNOTES_PARAMETER = "useCondensedFootnotes"
4625
private const val GSA_TO_IMPERSONATE_PARAMETER = "gsaToImpersonate"
26+
private const val INTRO_MESSAGE_PARAMETER = "introMessage"
4727

4828
internal object GoogleChatConnectorProvider : ConnectorProvider {
4929

@@ -90,12 +70,16 @@ internal object GoogleChatConnectorProvider : ConnectorProvider {
9070
?: error("Parameter Bot project number not present")
9171
)
9272

73+
val introMessage =
74+
connectorConfiguration.parameters[INTRO_MESSAGE_PARAMETER]?.takeIf { it.isNotBlank() }
75+
9376
return GoogleChatConnector(
9477
connectorId,
9578
path,
9679
chatService,
9780
authorisationHandler,
98-
useCondensedFootnotes
81+
useCondensedFootnotes,
82+
introMessage
9983
)
10084
}
10185
}
@@ -104,12 +88,7 @@ internal object GoogleChatConnectorProvider : ConnectorProvider {
10488
connectorConfiguration: ConnectorConfiguration,
10589
targetServiceAccount: String
10690
): GoogleCredentials {
107-
10891
val sourceCredentials = getSourceCredentials(connectorConfiguration)
109-
110-
logger.info { "Source credentials: ${(sourceCredentials as? ServiceAccountCredentials)?.clientEmail}" }
111-
logger.info { "Impersonating target GSA = $targetServiceAccount with scopes = $CHAT_SCOPE" }
112-
11392
return ImpersonatedCredentials.create(
11493
sourceCredentials,
11594
targetServiceAccount,
@@ -123,14 +102,9 @@ internal object GoogleChatConnectorProvider : ConnectorProvider {
123102
private fun getSourceCredentials(connectorConfiguration: ConnectorConfiguration): GoogleCredentials {
124103
return try {
125104
val credentialInputStream = getCredentialInputStream(connectorConfiguration)
126-
val creds = ServiceAccountCredentials.fromStream(credentialInputStream)
105+
ServiceAccountCredentials.fromStream(credentialInputStream)
127106
.createScoped("https://www.googleapis.com/auth/cloud-platform")
128-
129-
logger.info { "Loaded explicit service account: ${(creds as ServiceAccountCredentials).clientEmail}" }
130-
131-
creds
132107
} catch (e: Exception) {
133-
logger.info { "No explicit credentials found, using Application Default Credentials" }
134108
GoogleCredentials.getApplicationDefault()
135109
.createScoped("https://www.googleapis.com/auth/cloud-platform")
136110
}
@@ -181,6 +155,11 @@ internal object GoogleChatConnectorProvider : ConnectorProvider {
181155
"Use condensed footnotes (true = 1, false = 0)",
182156
CONDENSED_FOOTNOTES_PARAMETER,
183157
false
158+
),
159+
ConnectorTypeConfigurationField(
160+
"Introductory message (sent only once per new session)",
161+
INTRO_MESSAGE_PARAMETER,
162+
false
184163
)
185164
),
186165
svgIcon = resourceAsString("/google_chat.svg")

0 commit comments

Comments
 (0)