feat(models): support bot metadata, canonical sessions, and group sync in Profile models (#973)
This commit is contained in:
2 files changed
+269
No files matched your search
@@ -1,9 +1,14 @@
|
|||||||
package com.m57.hermescontrol.data.model
|
package com.m57.hermescontrol.data.model
|
||||||
|
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
import kotlinx.serialization.json.JsonElement
|
||||||
|
import kotlinx.serialization.json.decodeFromJsonElement
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class ProfilesResponse(
|
data class ProfilesResponse(
|
||||||
val profiles: List<ProfileInfo>,
|
val profiles: List<ProfileInfo>,
|
||||||
|
val bot_mode_protocol: Boolean? = null,
|
||||||
)
|
)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
@@ -17,7 +22,111 @@ data class ProfileInfo(
|
|||||||
val skill_count: Int? = null,
|
val skill_count: Int? = null,
|
||||||
val gateway_running: Boolean? = null,
|
val gateway_running: Boolean? = null,
|
||||||
val description: String? = null,
|
val description: String? = null,
|
||||||
|
val display_name: String? = null,
|
||||||
val description_auto: Boolean? = null,
|
val description_auto: Boolean? = null,
|
||||||
|
val has_avatar: Boolean? = null,
|
||||||
|
val ui_meta: Map<String, JsonElement>? = null,
|
||||||
|
val ui_meta_revisions: Map<String, Int>? = null,
|
||||||
|
val canonical_session: CanonicalSessionInfo? = null,
|
||||||
|
val last_session: ProfileSessionSummary? = null,
|
||||||
|
val worker_session: ProfileWorkerSummary? = null,
|
||||||
|
) {
|
||||||
|
fun botMeta(
|
||||||
|
json: Json =
|
||||||
|
Json {
|
||||||
|
ignoreUnknownKeys = true
|
||||||
|
isLenient = true
|
||||||
|
},
|
||||||
|
): BotRosterMeta? {
|
||||||
|
val element = ui_meta?.get("hermes-bots") ?: return null
|
||||||
|
return try {
|
||||||
|
json.decodeFromJsonElement<BotRosterMeta>(element)
|
||||||
|
} catch (_: Exception) {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val effectiveTitle: String
|
||||||
|
get() =
|
||||||
|
botMeta()?.title?.takeIf { it.isNotBlank() }
|
||||||
|
?: display_name?.takeIf { it.isNotBlank() }
|
||||||
|
?: name
|
||||||
|
|
||||||
|
val effectiveDescription: String
|
||||||
|
get() =
|
||||||
|
botMeta()?.description?.takeIf { it.isNotBlank() }
|
||||||
|
?: description
|
||||||
|
?: ""
|
||||||
|
|
||||||
|
val isHidden: Boolean
|
||||||
|
get() = botMeta()?.hidden == true
|
||||||
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class CanonicalSessionInfo(
|
||||||
|
val id: String,
|
||||||
|
val resolved_id: String? = null,
|
||||||
|
val root_title: String? = null,
|
||||||
|
val title: String? = null,
|
||||||
|
val preview: String? = null,
|
||||||
|
val started_at: Long? = null,
|
||||||
|
val last_active: Long? = null,
|
||||||
|
val message_count: Int? = null,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class ProfileSessionSummary(
|
||||||
|
val id: String,
|
||||||
|
val title: String? = null,
|
||||||
|
val preview: String? = null,
|
||||||
|
val started_at: Long? = null,
|
||||||
|
val last_active: Long? = null,
|
||||||
|
val message_count: Int? = null,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class ProfileWorkerSummary(
|
||||||
|
val id: String,
|
||||||
|
val source: String? = null,
|
||||||
|
val title: String? = null,
|
||||||
|
val last_active: Long? = null,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class BotRosterMeta(
|
||||||
|
val title: String? = null,
|
||||||
|
val description: String? = null,
|
||||||
|
val avatar: BotAvatarMeta? = null,
|
||||||
|
val hidden: Boolean? = null,
|
||||||
|
val groups: List<String>? = null,
|
||||||
|
val group: String? = null,
|
||||||
|
val created: Long? = null,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class BotAvatarMeta(
|
||||||
|
val shape: String? = null,
|
||||||
|
val color: String? = null,
|
||||||
|
val icon: String? = null,
|
||||||
|
val image_url: String? = null,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class GroupChatSyncSnapshot(
|
||||||
|
val version: Int? = null,
|
||||||
|
val updatedAt: Long? = null,
|
||||||
|
val rooms: Map<String, GroupChatRoomMeta>? = null,
|
||||||
|
val deleted: Map<String, Long>? = null,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class GroupChatRoomMeta(
|
||||||
|
val id: String,
|
||||||
|
val name: String? = null,
|
||||||
|
val members: List<String>? = null,
|
||||||
|
val picture: String? = null,
|
||||||
|
val updatedAt: Long? = null,
|
||||||
|
val createdAt: Long? = null,
|
||||||
)
|
)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
|
|||||||
@@ -0,0 +1,160 @@
|
|||||||
|
package com.m57.hermescontrol.data.model
|
||||||
|
|
||||||
|
import com.m57.hermescontrol.data.remote.OkHttpProvider
|
||||||
|
import kotlinx.serialization.decodeFromString
|
||||||
|
import kotlinx.serialization.encodeToString
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
|
import org.junit.Assert.assertFalse
|
||||||
|
import org.junit.Assert.assertNotNull
|
||||||
|
import org.junit.Assert.assertNull
|
||||||
|
import org.junit.Assert.assertTrue
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
class ProfileSerializationTest {
|
||||||
|
private val json = OkHttpProvider.json
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testProfileInfoDeserializationWithBotMetadata() {
|
||||||
|
val jsonStr =
|
||||||
|
"""
|
||||||
|
{
|
||||||
|
"name": "researcher",
|
||||||
|
"path": "/home/user/.hermes/profiles/researcher",
|
||||||
|
"is_default": false,
|
||||||
|
"model": "anthropic/claude-3-7-sonnet",
|
||||||
|
"provider": "anthropic",
|
||||||
|
"skill_count": 5,
|
||||||
|
"description": "Base description",
|
||||||
|
"display_name": "Researcher Agent",
|
||||||
|
"has_avatar": true,
|
||||||
|
"canonical_session": {
|
||||||
|
"id": "sess-canon-123",
|
||||||
|
"resolved_id": "sess-canon-tip",
|
||||||
|
"root_title": "Bot Chat",
|
||||||
|
"title": "Bot Chat",
|
||||||
|
"preview": "Latest research report ready.",
|
||||||
|
"started_at": 1700000000,
|
||||||
|
"last_active": 1700005000,
|
||||||
|
"message_count": 42
|
||||||
|
},
|
||||||
|
"worker_session": {
|
||||||
|
"id": "worker-456",
|
||||||
|
"source": "kanban",
|
||||||
|
"title": "Background analysis",
|
||||||
|
"last_active": 1700006000
|
||||||
|
},
|
||||||
|
"last_session": {
|
||||||
|
"id": "last-789",
|
||||||
|
"title": "Ad-hoc task",
|
||||||
|
"preview": "Done!",
|
||||||
|
"started_at": 1700001000,
|
||||||
|
"last_active": 1700002000,
|
||||||
|
"message_count": 10
|
||||||
|
},
|
||||||
|
"ui_meta": {
|
||||||
|
"hermes-bots": {
|
||||||
|
"title": "Dr. Researcher",
|
||||||
|
"description": "Deep literature scout",
|
||||||
|
"avatar": {
|
||||||
|
"shape": "hexagon",
|
||||||
|
"color": "#4A90E2",
|
||||||
|
"icon": "science",
|
||||||
|
"image_url": "https://example.com/avatar.png"
|
||||||
|
},
|
||||||
|
"hidden": false,
|
||||||
|
"groups": ["science-team", "daily-sync"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""".trimIndent()
|
||||||
|
|
||||||
|
val profile = json.decodeFromString<ProfileInfo>(jsonStr)
|
||||||
|
assertEquals("researcher", profile.name)
|
||||||
|
assertEquals("Researcher Agent", profile.display_name)
|
||||||
|
assertTrue(profile.has_avatar == true)
|
||||||
|
|
||||||
|
// Canonical session
|
||||||
|
val canon = profile.canonical_session
|
||||||
|
assertNotNull(canon)
|
||||||
|
assertEquals("sess-canon-123", canon?.id)
|
||||||
|
assertEquals("sess-canon-tip", canon?.resolved_id)
|
||||||
|
assertEquals("Latest research report ready.", canon?.preview)
|
||||||
|
assertEquals(42, canon?.message_count)
|
||||||
|
|
||||||
|
// Worker session
|
||||||
|
val worker = profile.worker_session
|
||||||
|
assertNotNull(worker)
|
||||||
|
assertEquals("worker-456", worker?.id)
|
||||||
|
assertEquals("kanban", worker?.source)
|
||||||
|
|
||||||
|
// Bot metadata decoding
|
||||||
|
val botMeta = profile.botMeta(json)
|
||||||
|
assertNotNull(botMeta)
|
||||||
|
assertEquals("Dr. Researcher", botMeta?.title)
|
||||||
|
assertEquals("Deep literature scout", botMeta?.description)
|
||||||
|
assertEquals("hexagon", botMeta?.avatar?.shape)
|
||||||
|
assertEquals("#4A90E2", botMeta?.avatar?.color)
|
||||||
|
assertEquals(listOf("science-team", "daily-sync"), botMeta?.groups)
|
||||||
|
|
||||||
|
// Helpers
|
||||||
|
assertEquals("Dr. Researcher", profile.effectiveTitle)
|
||||||
|
assertEquals("Deep literature scout", profile.effectiveDescription)
|
||||||
|
assertFalse(profile.isHidden)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testProfileInfoBackwardCompatibility() {
|
||||||
|
val legacyJson =
|
||||||
|
"""
|
||||||
|
{
|
||||||
|
"name": "default",
|
||||||
|
"is_default": true,
|
||||||
|
"model": "openai/gpt-4o",
|
||||||
|
"provider": "openai",
|
||||||
|
"description": "Default Assistant"
|
||||||
|
}
|
||||||
|
""".trimIndent()
|
||||||
|
|
||||||
|
val profile = json.decodeFromString<ProfileInfo>(legacyJson)
|
||||||
|
assertEquals("default", profile.name)
|
||||||
|
assertTrue(profile.is_default == true)
|
||||||
|
assertNull(profile.canonical_session)
|
||||||
|
assertNull(profile.worker_session)
|
||||||
|
assertNull(profile.last_session)
|
||||||
|
assertNull(profile.ui_meta)
|
||||||
|
assertNull(profile.botMeta(json))
|
||||||
|
assertEquals("default", profile.effectiveTitle)
|
||||||
|
assertEquals("Default Assistant", profile.effectiveDescription)
|
||||||
|
assertFalse(profile.isHidden)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testGroupChatSyncSnapshotSerialization() {
|
||||||
|
val snapshot =
|
||||||
|
GroupChatSyncSnapshot(
|
||||||
|
version = 3,
|
||||||
|
updatedAt = 1724000000L,
|
||||||
|
rooms =
|
||||||
|
mapOf(
|
||||||
|
"room-1" to
|
||||||
|
GroupChatRoomMeta(
|
||||||
|
id = "room-1",
|
||||||
|
name = "War Room",
|
||||||
|
members = listOf("default", "researcher"),
|
||||||
|
picture = null,
|
||||||
|
updatedAt = 1724000000L,
|
||||||
|
createdAt = 1723000000L,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
deleted = mapOf("old-room" to 1723500000L),
|
||||||
|
)
|
||||||
|
|
||||||
|
val encoded = json.encodeToString(snapshot)
|
||||||
|
val decoded = json.decodeFromString<GroupChatSyncSnapshot>(encoded)
|
||||||
|
assertEquals(3, decoded.version)
|
||||||
|
assertEquals(1, decoded.rooms?.size)
|
||||||
|
assertEquals("War Room", decoded.rooms?.get("room-1")?.name)
|
||||||
|
assertEquals(listOf("default", "researcher"), decoded.rooms?.get("room-1")?.members)
|
||||||
|
assertEquals(1723500000L, decoded.deleted?.get("old-room"))
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user