diff --git a/ci/ci-values.yaml b/ci/ci-values.yaml
index 05eda6c164..fa78a9dc74 100644
--- a/ci/ci-values.yaml
+++ b/ci/ci-values.yaml
@@ -54,4 +54,4 @@ postgresql:
- "management"
ingress:
- className: traefik
\ No newline at end of file
+ className: traefik
diff --git a/forge/db/views/ProjectTemplate.js b/forge/db/views/ProjectTemplate.js
index 3d9fef4c24..dd2912a897 100644
--- a/forge/db/views/ProjectTemplate.js
+++ b/forge/db/views/ProjectTemplate.js
@@ -16,7 +16,7 @@ module.exports = function (app) {
name: result.name,
description: result.description,
active: result.active,
- instanceCount: result.projectCount,
+ instanceCount: parseInt(result.projectCount) || 0,
settings: result.settings || {},
policy: result.policy || {},
createdAt: result.createdAt,
@@ -56,7 +56,7 @@ module.exports = function (app) {
name: result.name,
description: result.description,
active: result.active,
- instanceCount: result.projectCount,
+ instanceCount: parseInt(result.projectCount) || 0,
createdAt: result.createdAt,
links: result.links
}
diff --git a/frontend/src/api/user.js b/frontend/src/api/user.js
index 21bf7c906a..3f1c21c37d 100644
--- a/frontend/src/api/user.js
+++ b/frontend/src/api/user.js
@@ -239,6 +239,9 @@ const disableMFA = async () => {
const verifyMFA = async (token) => {
return client.put('/api/v1/user/mfa/verify', { token }).then(res => res.data)
}
+const initiateExpertChat = async ({ sessionId }) => {
+ return client.post('/api/v1/user/expert-creds', { sessionId }).then(res => res.data)
+}
export default {
registerUser,
@@ -267,5 +270,6 @@ export default {
updatePersonalAccessToken,
enableMFA,
verifyMFA,
- disableMFA
+ disableMFA,
+ initiateExpertChat
}
diff --git a/frontend/src/components/expert/Expert.vue b/frontend/src/components/expert/Expert.vue
index f40c17ab36..406248a9ea 100644
--- a/frontend/src/components/expert/Expert.vue
+++ b/frontend/src/components/expert/Expert.vue
@@ -148,7 +148,8 @@ export default {
'setAgentMode',
'setAbortController',
'resetSessionTimer',
- 'addWelcomeMessageIfNeeded'
+ 'addWelcomeMessageIfNeeded',
+ 'stopInflightChat'
]),
...mapActions(useProductExpertInsightsAgentStore, ['getCapabilities']),
...mapActions(useProductAssistantStore, ['reset']),
@@ -157,6 +158,7 @@ export default {
this.abortController.abort()
this.setAbortController(null)
}
+ this.stopInflightChat()
},
handleScroll () {
// Debounce scroll detection
diff --git a/frontend/src/components/expert/components/ExpertChatInput.vue b/frontend/src/components/expert/components/ExpertChatInput.vue
index 9a0a61ce61..9238db99f2 100644
--- a/frontend/src/components/expert/components/ExpertChatInput.vue
+++ b/frontend/src/components/expert/components/ExpertChatInput.vue
@@ -33,7 +33,7 @@
/>
-
+
@@ -64,13 +64,13 @@
+
+
diff --git a/frontend/src/components/expert/components/messages/components/ToolCalls.vue b/frontend/src/components/expert/components/messages/components/ToolCalls.vue
index 3943d8106a..565d2b7dd7 100644
--- a/frontend/src/components/expert/components/messages/components/ToolCalls.vue
+++ b/frontend/src/components/expert/components/messages/components/ToolCalls.vue
@@ -9,70 +9,27 @@
{{ formattedDuration }} ms
-
-
+
- {{ tool.title || tool.name }}
-
- {{ formatKindBadge(tool.kind) }}
- {{ tool.name }}
-
-
-
+ :tool="tool"
+ :expanded="expanded"
+ />
diff --git a/frontend/src/components/expert/components/messages/components/resources/StreamableContent.vue b/frontend/src/components/expert/components/messages/components/resources/StreamableContent.vue
index 436250a8cc..43bdb2ab8f 100644
--- a/frontend/src/components/expert/components/messages/components/resources/StreamableContent.vue
+++ b/frontend/src/components/expert/components/messages/components/resources/StreamableContent.vue
@@ -222,5 +222,24 @@ export default {
background-color: $ff-color--highlight;
}
}
+
+ :deep(code) {
+ padding: 0;
+ border: none;
+ border-radius: 0;
+ }
+
+ :deep(table) {
+ border-collapse: collapse; /* removes double borders */
+
+ tr + tr td {
+ font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace;
+ border-top: 1px solid $ff-grey-200;
+ }
+
+ td, th {
+ padding: 5px 10px;
+ }
+ }
}
diff --git a/frontend/src/composables/services/MqttExpertTopicHelper.ts b/frontend/src/composables/services/MqttExpertTopicHelper.ts
new file mode 100644
index 0000000000..cb5a71b6ad
--- /dev/null
+++ b/frontend/src/composables/services/MqttExpertTopicHelper.ts
@@ -0,0 +1,138 @@
+import { useAccountAuthStore, useContextStore, useProductExpertStore } from '@/stores'
+
+interface EntityWithId {
+ id: string
+ [key: string]: unknown
+}
+
+interface EntityTopicPathsOptions {
+ application?: EntityWithId | null
+ instance?: EntityWithId | null
+ device?: EntityWithId | null
+ team?: EntityWithId | null
+}
+
+interface EntityTopicPaths {
+ entityType: 'a' | 'p' | 'd' | 't'
+ entityId: string | undefined
+}
+
+type AgentChannel = 'support' | 'insights'
+type TopicType = 'chat' | 'inflight'
+type TopicAction = 'response' | 'request'
+
+interface ParsedTopic {
+ topic: string
+ isReply: boolean
+ isInflightRequest: boolean
+ entityType: string
+ entityId: string
+ agentChannel: 'support' | 'insights' | string
+ topicType: string | 'chat' | 'inflight' | null
+ topicAction: string | 'request' | 'response' | null
+ inflightType: string | null
+}
+
+interface BuildTopicOptions {
+ entityType?: EntityTopicPaths['entityType'] | null
+ entityId?: string | null
+ agentChannel?: AgentChannel
+ topicType?: TopicType
+ topicAction?: TopicAction,
+ inflightType?: string | null
+}
+
+export function useMqttExpertTopicHelper () {
+ function getEntityTopicPaths (options?: EntityTopicPathsOptions): EntityTopicPaths {
+ const { application, instance, device, team } = options ?? {}
+ const contextStore = useContextStore()
+
+ switch (true) {
+ case !!application || !!contextStore.application:
+ return {
+ entityType: 'a',
+ entityId: application?.id ?? contextStore.application?.id
+ }
+ case !!instance || !!contextStore.instance:
+ return {
+ entityType: 'p',
+ entityId: instance?.id ?? contextStore.instance.id
+ }
+ case !!device || !!contextStore.device:
+ return {
+ entityType: 'd',
+ entityId: device?.id ?? contextStore.device?.id
+ }
+ default:
+ return {
+ entityType: 't',
+ entityId: team?.id ?? contextStore.team?.id
+ }
+ }
+ }
+
+ function buildTopic (options?: BuildTopicOptions): string {
+ const { entityType, entityId, agentChannel, topicType, topicAction, inflightType } = options ?? {}
+
+ if (!entityType) throw new Error('Topic "entityType" is mandatory')
+ if (!entityId) throw new Error('Topic "entityId" is mandatory')
+ if (!agentChannel || !['support', 'insights'].includes(agentChannel)) {
+ throw new Error(`"agentChannel" must be either "support" or "insights", "${agentChannel}" given`)
+ }
+ if (!topicType || !['chat', 'inflight'].includes(topicType)) {
+ throw new Error(`"topicType" must be either "chat" or "inflight", "${topicType}" given`)
+ }
+ if (!topicAction || !['response', 'request'].includes(topicAction)) {
+ throw new Error(`"topicAction" must be either "response" or "request", "${topicAction}" given`)
+ }
+
+ const authStore = useAccountAuthStore()
+ const expertStore = useProductExpertStore()
+
+ const sessionId = expertStore.sessionId
+
+ return [
+ 'ff',
+ 'v1',
+ 'expert',
+ authStore.user.id,
+ sessionId,
+ entityType,
+ entityId,
+ agentChannel,
+ topicType,
+ inflightType,
+ topicAction
+ ]
+ .filter(str => str).join('/')
+ }
+
+ function parseTopic (topic: string): ParsedTopic {
+ // topic examples
+ // ff/v1/expert///////
+ // ff/v1/expert////////
+
+ if (!topic || topic.length === 0) throw new Error(`Invalid topic received: "${topic}"`)
+
+ const split = topic.split('/')
+
+ const inflightRequest = topic.includes('/inflight/') && topic.endsWith('/request')
+ return {
+ topic,
+ isReply: topic.endsWith('/response'),
+ isInflightRequest: inflightRequest,
+ entityType: split[5],
+ entityId: split[6],
+ agentChannel: split[7],
+ topicType: split[8],
+ topicAction: split.at(-1),
+ inflightType: inflightRequest ? split.at(-2) ?? null : null
+ }
+ }
+
+ return {
+ getEntityTopicPaths,
+ buildTopic,
+ parseTopic
+ }
+}
diff --git a/frontend/src/mixins/Application.js b/frontend/src/mixins/Application.js
index e744829d7e..7812450172 100644
--- a/frontend/src/mixins/Application.js
+++ b/frontend/src/mixins/Application.js
@@ -1,3 +1,5 @@
+import { mapActions } from 'pinia'
+
import ApplicationApi from '../api/application.js'
import alerts from '../services/alerts.js'
@@ -26,8 +28,14 @@ export default {
}
},
watch: {
+ application (application) {
+ if (application) {
+ this.setContextualApplication(application)
+ }
+ }
},
methods: {
+ ...mapActions(useContextStore, { setContextualApplication: 'setApplication' }),
async updateApplication () {
const applicationId = this.$route.params.id
@@ -96,5 +104,8 @@ export default {
},
async created () {
+ },
+ beforeUnmount () {
+ this.setContextualApplication(null)
}
}
diff --git a/frontend/src/mixins/Instance.js b/frontend/src/mixins/Instance.js
index 99e2db4bbf..7a2d00b903 100644
--- a/frontend/src/mixins/Instance.js
+++ b/frontend/src/mixins/Instance.js
@@ -1,4 +1,4 @@
-import { mapState } from 'pinia'
+import { mapActions, mapState } from 'pinia'
import InstanceApi from '../api/instances.js'
import SnapshotApi from '../api/projectSnapshots.js'
@@ -34,9 +34,13 @@ export default {
}
},
watch: {
- instance: 'instanceChanged'
+ instance (instance) {
+ this.instanceChanged()
+ this.setContextualInstance(instance)
+ }
},
methods: {
+ ...mapActions(useContextStore, { setContextualInstance: 'setInstance' }),
showConfirmDeleteDialog () {
this.$refs.confirmInstanceDeleteDialog.show(this.instance)
},
@@ -106,5 +110,8 @@ export default {
await this.loadInstance()
}
)
+ },
+ beforeUnmount () {
+ this.setContextualInstance(null)
}
}
diff --git a/frontend/src/pages/admin/Settings/General.vue b/frontend/src/pages/admin/Settings/General.vue
index f99877b019..928c75d1ec 100644
--- a/frontend/src/pages/admin/Settings/General.vue
+++ b/frontend/src/pages/admin/Settings/General.vue
@@ -157,7 +157,7 @@
Disable
-
+
Allow Expert agent to connect to the platform
@@ -466,7 +466,7 @@ export default {
// The Expert Agent Credentials option in the admin UI is only ever supposed to be shown on
// FFC platforms. If the feature flag is retired, we will need to gate this some other way.
- if (this.featuresCheck?.isExpertCommsBetaEnabled) {
+ if (this.featuresCheck?.isPostHogFeatureFlagsEnabled) {
this.expertAgentEnabled = this.input['platform:expert-agent:creds']
if (!this.expertAgentEnabled) {
this.expertAgentCreds = ''
diff --git a/frontend/src/pages/application/index.vue b/frontend/src/pages/application/index.vue
index 3b5b649626..9d63f3eb0b 100644
--- a/frontend/src/pages/application/index.vue
+++ b/frontend/src/pages/application/index.vue
@@ -31,13 +31,9 @@