|
| 1 | +package ai.koog.agents.features.acp |
| 2 | + |
| 3 | +import ai.koog.prompt.message.AttachmentContent |
| 4 | +import ai.koog.prompt.message.ContentPart |
| 5 | +import ai.koog.prompt.message.Message |
| 6 | +import ai.koog.prompt.message.RequestMetaInfo |
| 7 | +import com.agentclientprotocol.common.Event.SessionUpdateEvent |
| 8 | +import com.agentclientprotocol.model.ContentBlock |
| 9 | +import com.agentclientprotocol.model.EmbeddedResourceResource |
| 10 | +import com.agentclientprotocol.model.SessionUpdate |
| 11 | +import com.agentclientprotocol.model.SessionUpdate.AgentMessageChunk |
| 12 | +import com.agentclientprotocol.model.ToolCallId |
| 13 | +import com.agentclientprotocol.model.ToolCallStatus |
| 14 | +import kotlinx.datetime.Clock |
| 15 | + |
| 16 | +private const val UNKNOWN_FORMAT = "unknown" |
| 17 | +private const val UNKNOWN_MIME_TYPE = "unknown/unknown" |
| 18 | +private const val UNKNOWN_URI = "unknown" |
| 19 | +private const val UNKNOWN_FILE_NAME = "unknown" |
| 20 | +private const val UNKNOWN_TOOL_CALL_ID = "unknown" |
| 21 | + |
| 22 | +private fun parseFormat(mimeType: String?): String { |
| 23 | + return mimeType?.split("/")?.lastOrNull() ?: UNKNOWN_FORMAT |
| 24 | +} |
| 25 | + |
| 26 | +/** |
| 27 | + * Converts a list of [ContentBlock] of ACP prompt to a Koog [Message.User]. |
| 28 | + */ |
| 29 | +public fun List<ContentBlock>.toKoogMessage(clock: Clock): Message { |
| 30 | + return Message.User( |
| 31 | + parts = this.map { it.toKoogContentPart() }, |
| 32 | + metaInfo = RequestMetaInfo(clock.now()) |
| 33 | + ) |
| 34 | +} |
| 35 | + |
| 36 | +/** |
| 37 | + * Converts a single [ContentBlock] of ACP prompt to a Koog [ContentPart]. |
| 38 | + */ |
| 39 | +public fun ContentBlock.toKoogContentPart(): ContentPart { |
| 40 | + return when (this) { |
| 41 | + // https://agentclientprotocol.com/protocol/content#audio-content |
| 42 | + is ContentBlock.Audio -> { |
| 43 | + ContentPart.Audio( |
| 44 | + content = AttachmentContent.Binary.Base64(data), |
| 45 | + format = parseFormat(mimeType), |
| 46 | + mimeType = mimeType |
| 47 | + ) |
| 48 | + } |
| 49 | + |
| 50 | + // https://agentclientprotocol.com/protocol/content#image-content |
| 51 | + is ContentBlock.Image -> { |
| 52 | + ContentPart.Image( |
| 53 | + content = AttachmentContent.Binary.Base64(data), |
| 54 | + format = parseFormat(mimeType), |
| 55 | + mimeType = mimeType |
| 56 | + ) |
| 57 | + } |
| 58 | + |
| 59 | + // https://agentclientprotocol.com/protocol/content#embedded-resource |
| 60 | + is ContentBlock.Resource -> { |
| 61 | + when (val resource = this.resource) { |
| 62 | + is EmbeddedResourceResource.BlobResourceContents -> { |
| 63 | + ContentPart.File( |
| 64 | + content = AttachmentContent.Binary.Base64(resource.blob), |
| 65 | + format = parseFormat(resource.mimeType), |
| 66 | + mimeType = resource.mimeType ?: UNKNOWN_MIME_TYPE |
| 67 | + ) |
| 68 | + } |
| 69 | + |
| 70 | + is EmbeddedResourceResource.TextResourceContents -> { |
| 71 | + ContentPart.File( |
| 72 | + content = AttachmentContent.PlainText(resource.text), |
| 73 | + format = parseFormat(resource.mimeType), |
| 74 | + mimeType = resource.mimeType ?: UNKNOWN_MIME_TYPE |
| 75 | + ) |
| 76 | + } |
| 77 | + } |
| 78 | + } |
| 79 | + |
| 80 | + // https://agentclientprotocol.com/protocol/content#resource-link |
| 81 | + is ContentBlock.ResourceLink -> { |
| 82 | + ContentPart.File( |
| 83 | + content = AttachmentContent.URL(uri), |
| 84 | + format = parseFormat(mimeType), |
| 85 | + mimeType = mimeType ?: UNKNOWN_MIME_TYPE |
| 86 | + ) |
| 87 | + } |
| 88 | + |
| 89 | + // https://agentclientprotocol.com/protocol/content#text-content |
| 90 | + is ContentBlock.Text -> { |
| 91 | + ContentPart.Text(text) |
| 92 | + } |
| 93 | + } |
| 94 | +} |
| 95 | + |
| 96 | +/** |
| 97 | + * Converts a [Message.Response] to a list of ACP [SessionUpdateEvent]. |
| 98 | + */ |
| 99 | +public fun Message.Response.toAcpEvents(): List<SessionUpdateEvent> { |
| 100 | + val response = this |
| 101 | + return buildList { |
| 102 | + when (response) { |
| 103 | + is Message.Assistant -> { |
| 104 | + response.parts.forEach { part -> |
| 105 | + add( |
| 106 | + SessionUpdateEvent( |
| 107 | + update = AgentMessageChunk(part.toAcpContentBlock()) |
| 108 | + ) |
| 109 | + ) |
| 110 | + } |
| 111 | + } |
| 112 | + |
| 113 | + is Message.Reasoning -> { |
| 114 | + add( |
| 115 | + SessionUpdateEvent( |
| 116 | + update = SessionUpdate.AgentThoughtChunk( |
| 117 | + content = ContentBlock.Text(response.content) |
| 118 | + ) |
| 119 | + ) |
| 120 | + ) |
| 121 | + } |
| 122 | + |
| 123 | + is Message.Tool.Call -> { |
| 124 | + add( |
| 125 | + SessionUpdateEvent( |
| 126 | + update = SessionUpdate.ToolCall( |
| 127 | + toolCallId = ToolCallId(response.id ?: UNKNOWN_TOOL_CALL_ID), |
| 128 | + // TODO: Support tool description in the event |
| 129 | + title = response.tool, |
| 130 | + // TODO: Support kind for tools |
| 131 | + status = ToolCallStatus.PENDING, |
| 132 | + rawInput = response.contentJson, |
| 133 | + ) |
| 134 | + ) |
| 135 | + ) |
| 136 | + } |
| 137 | + } |
| 138 | + } |
| 139 | +} |
| 140 | + |
| 141 | +/** |
| 142 | + * Converts a ContentPart to an ACP ContentBlock. |
| 143 | + */ |
| 144 | +public fun ContentPart.toAcpContentBlock(): ContentBlock { |
| 145 | + return when (this) { |
| 146 | + is ContentPart.Text -> { |
| 147 | + ContentBlock.Text(this.text) |
| 148 | + } |
| 149 | + |
| 150 | + is ContentPart.Audio -> { |
| 151 | + ContentBlock.Audio( |
| 152 | + data = this.content.toString(), |
| 153 | + mimeType = this.mimeType, |
| 154 | + ) |
| 155 | + } |
| 156 | + |
| 157 | + is ContentPart.File -> |
| 158 | + when (val content = this.content) { |
| 159 | + is AttachmentContent.Binary.Base64 -> ContentBlock.Resource( |
| 160 | + resource = EmbeddedResourceResource.BlobResourceContents( |
| 161 | + blob = content.base64, |
| 162 | + // TODO: add uri to the file |
| 163 | + uri = UNKNOWN_URI, |
| 164 | + mimeType = this.mimeType |
| 165 | + ) |
| 166 | + ) |
| 167 | + |
| 168 | + is AttachmentContent.Binary.Bytes -> ContentBlock.Resource( |
| 169 | + resource = EmbeddedResourceResource.BlobResourceContents( |
| 170 | + blob = content.asBase64(), |
| 171 | + // TODO: add uri to the file |
| 172 | + uri = UNKNOWN_URI, |
| 173 | + mimeType = this.mimeType |
| 174 | + ) |
| 175 | + ) |
| 176 | + |
| 177 | + is AttachmentContent.PlainText -> ContentBlock.Resource( |
| 178 | + resource = EmbeddedResourceResource.TextResourceContents( |
| 179 | + text = content.text, |
| 180 | + // TODO: add uri to the file |
| 181 | + uri = UNKNOWN_URI |
| 182 | + ) |
| 183 | + ) |
| 184 | + |
| 185 | + is AttachmentContent.URL -> { |
| 186 | + ContentBlock.ResourceLink( |
| 187 | + name = this.fileName ?: UNKNOWN_FILE_NAME, |
| 188 | + uri = content.url |
| 189 | + ) |
| 190 | + } |
| 191 | + } |
| 192 | + |
| 193 | + is ContentPart.Image -> { |
| 194 | + ContentBlock.Image( |
| 195 | + data = this.content.toString(), |
| 196 | + mimeType = this.mimeType, |
| 197 | + ) |
| 198 | + } |
| 199 | + |
| 200 | + is ContentPart.Video -> { |
| 201 | + throw AcpException("Video content is not supported yet in Acp content blocks.") |
| 202 | + } |
| 203 | + } |
| 204 | +} |
0 commit comments